Files
n8n-enterprise-unlocked/packages/editor-ui/src/components/RunData.vue
Ben Hesseldieck 97cd564f7b feat(cli): User Management and Credentials sharing (#3602)
* 🎉 starting feature development

*  sharing/unsharing a credential (#3601)

* 🎉 initial design

*  sharing/unsharing of credentials

*  add tests for EE credentials controller

* 💪 implement review comments

* 🛠 refactor agent creation and credential role locking

* 👕 linting adjustments (#3691)

* 👕 Adjust rule `naming-convention`

* 👕 Fix `naming-convention` config value

* 👕 Disregard casing for EE-prefixed vars

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* 🛠 refactor authAgents in tests (#3725)

* 🛠 refactor authAgent

* 🛠 refactor authAgent

* 🛠 refactor authAgent

* 🛠 refactor authAgent

* 👕 fix ts issue

* 🐘 add migration for mysql and postgres + add AuthAgent type

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>

* ️ refactor existing credentials routes (#3672)

* 🎉 initial design

*  sharing/unsharing of credentials

* ♻️ split credential update route into controller and service

* 🔥 remove credentials test that is no longer applicable

* ♻️ split credential creation route into controller and service

* ♻️ split single credential get

* ♻️ split delete credentials route

* ♻️ split get all credentials route

* 🔥 remove unused imports in credentials contoller

* 🔥 remove console.log

* :refactor: changes to credentials controller and service from review

 - removed credentials from service function names
 - made relations list optional
 - put allowGlobalOwner in options objects
 - check length of relations array so join doesn't happen if empty
 - update some comments to further explain rationale
 - remove unneeded `Object.assign`
 - remove non-null assertion from test

* ♻️ move filtered credentials selected fields to variable

* ♻️ remove unneeded merges in credentials service

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>

*  fix test

* 🐛 fix imports

* 👕 fix lint issue

* User Management: switch over to decorators to define routes (#3827)

* Add permissions details to credentials for User Management (#3863)

*  Open `GET /users`

*  Add permissions to cred service

* 🚚 Rename method

*  Refactor cred controller

* 🧪 Adjust test

* ✏️ Improve comment

* ✏️ Improve another comment

*  Account for multiple sharings

* 🐛 Fix access when user is editor

* 📘 Expand interface

* 📘 Relocate types

* 📘 Exempt cred entity with service-injected fields

* 📘 Adjust interface

* ♻️ Add permissions only in `GET /credentials`

* 🧪 Add expectations for `ownedBy`

* 🧪 Add sharing details test

* 🧪 Make `ownedBy` checks more granular

* 📘 Adjust interface

* 🚚 Rename cred getter

* ♻️ Refactor cred getter

* 🧪 Expand tests

* ♻️ Refactor to use guard

* 👕 Remove unneeded lint exception

* 🔥 Remove unneeded relation

* 🚚 Move relation to `GET /credentials/:id`

* 📘 Consolidate typings

* 🎨 Add multiline for readability

* 🔥 Remove unneeded type

* ✏️ Clarity comment

* ✏️ Make comments consistent

* 👕 Add exception to fix build

* 👕 Add more lint exceptions to fix build

* 🐛 Check for non-owner

* 📘 Improve typings

* 🧪 Temporarily skip tests

* 🔥 Remove `@ts-ignore`

* 👕 Move lint exceptions

* ♻️ Refactor cred service and controller

*  Simplify check

* ✏️ adjust naming to experimental

* ️ add credentialsSharing flag to settings

* 🛠 add helper to check if UM is also enabled as dependency for CredentialsSharing

* 👕 fix lint error

* 🐘 change name of credential role

* 🚧 WIP batch sharing

* 🚧 WIP use put for sharing

*  add tests for batch sharing, 🛠 implement review suggestions

*  expand credential sharing tests for User Management (#3931)

* 🧪 Expand cred sharing tests

*  Add recently added flags

*  fix and adjust tests for /credentials

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>

*  User management v2 Front End (#3795)

* feat: Added responsive generic page view layout.

* feat: Added empty state.

* feat: Added credentials view empty state.

* test: Added unit tests for N8nActionBox

* feat: Added credentials list initial design.

* feat: Added credential actions. Started working on filters.

* feat: Updated InfoTip markup, added tests and changed stories to typescript.

* feat: Added credentials filtering by type. Added support for apply/reset filters.

* feat: Added credential sharing user select and user list. Added paywall component.

* feat: Updated credentials view permissions.

* feat: Added support for temporary sharing config for unsaved credentials.

* test: Fixed broken snapshots.

* feat: Added overflow styles to page-view-layout list.

* feat: Handled sharee specific views.

* feat: Integration between FE and BE to support real-world credential sharing scenario.

* feat: Added front end permissions table.

* feat: Refactored credential sharing flow. Updated design elements.

* feat: Added margin and padding auto spacer utilities.

* feat: Rehauled permissions to support instanceOwner role and action inheritance.

* feat: Updated credentials view to apply filters automatically.

* feat: Removed apply filters button and added active button state.

* test: Updated component snapshots.

* refactor: Renamed ResourceSharee to ResourceReader.

* feat: Credential sharing error handling, permissions improvement.

* feat: Updated permissions and error handling.

* chore: Removed console.log.

* 🛠 refactor enabling of credentialsSharing

* feat: Removed owner menu selector from credentials when sharing is disabled.

* refactor: Moved EE features into ee store module file.

* 🛠 add sharing info to GET credentials/:id

* fix: Fixed initial credential data loading for sharing.

* chore: Removed console.log.

* 🐛 owner can fetch any credential

* 🛠 refactor users test

* 👕 fix build type issue

* fix: Removed owner tag when credential sharing is disabled. Fixed small reactivity issue.

* chore: Removed console.log.

* 🚧 separate fetching credentials between EE and open

* fix: Fixed empty dropdown in users list.

* fix: Fixed error message and initialization when credential gets unshared.

*  add tests for fetching single credential

* Revert decorators based controllers

* ️ adjust credentials test route to also allow testing for sharees (#3999)

* ️ pull data if user is sharee

* fix: Removed sharedWith and ownedBy from credentialData on testing credentials.

Co-authored-by: Alex Grozav <alex@grozav.com>

* 📈 add BE analytics

* 💪 improve credential test

* ️ adjust tracking properties

* ️ removed roles from tracking

* 🐛 fix build by removing imports

* 🐛 fix missed merge conflict

* feat: User management P2 Front End bug bash and improvements (#4014)

* fix: Fixed type select size after reopening dropdown.

* fix: Fixed template cards.

* fix: Fixed card content size and copy input.

* fix: Fixed horizontal overflow.

* fix: Hiding el-tags scrollbar in select.

* fix: Added fallback credential icon. Added oAuth credential owner check.

* feat: Added disabled state to user select.

* feat: Added fallback scenario for non-existent credential types.

* feat: Adjusted credentials empty state to show that there are shared credentials.

* fix: Fixed time title.

* feat: Added actionable empty state when shared credentials are present.

* fix: Made action box x padding smaller

* feat: Repositioned owner tag for credential card.

* feat: Updated message box styling to use n8n css variables.

* feat: Added confirmation for deleting sharee.

* fix: Fixed deleted credential types. Fixed select in dropdown bug.

* fix: Various code improvements. Addressed PR review comments.

* fix: Fixed credential deletion errors.

* fix: Various code quality improvements.

* feat: N8N-4531 update cloud coming soon features (#4025)

* feat: Showing different upcoming feature messages and format for cloud.

* fix: Changed url format.

* fix: Updated how cloud deployment is determined.

* feat: N8N-4527 implementing credential sharing FE telemetry (#4023)

* feat: Added credential sharing telemetry.

* chore: Renamed computed function for consistency.

* refactor: Simplified subview telemetry sending.

* fix: Changed to callDebounced() helper.

* 📧 update email text

* fix: Adjusted feature coming soon margin.

* chore: Fixed type and line height for delete sharee confirmation modal.

* refactor(editor-ui): Update telemetry (#4040)

* 🔥 Remove `identify` from BE

*  Add `versionCli`

*  Add node creator ignore input

*  Move obfuscators to editor-ui

*  Refactor `ph-no-capture`

*  Pass `user_id` to manual exec props

* 🚚 Relocate class in `SettingsApiView`

*  Add `userId` to BE PH `identify` call

*  Revert " Add `userId` to BE PH `identify` call"

This reverts commit 895aaa45e51506d5dbdcbdabe249a2c743d8e468.

* Revert " Revert " Add `userId` to BE PH `identify` call""

This reverts commit b86a098c202155742c927c88c04c971a5d34dce5.

* 🐛 Fix `Promise` handling in `track()` call

*  Restore `Db.collections` call

*  Set up PH payload to mirror RS

* 🔥 Remove excess `userId`

* 📘 Remove `userId` from interface

* 🔥 Remove unused ref and method

* fix: Fixed bug causing instanceOwner to become credential owner on update. (#4079)

* 🐛 fix test for credential shared with member

* 👕 fix lint issues

* delete conflicting migration. this data is already seeded in CreateUserManagement

* feat: Expand obfuscation to User Management credential sharing (#4070)

 Expand obfuscation

* feat: Added credential sharing infotip for instance owner.

* bring back the migration. add a check to avoid conflicts on inserts

* fix(cli): use a non-env config flag to detect of enterprise features are enabled (#4105)

* chore: Changed ampersand to and in translation.

* refactor(telemetry): Obfuscate code and JSON editors (#4118)

 Obfuscate code and JSON editors

* feat(editor): improve design and functionality of coming soon features (#4116)

* feat: Improved coming soon feature design and functionality.

* style: Removed empty line.

* chore: Removed unused translation.

* fix: fix telemetry for credential creates and updates (#4125)

fix telemetry for credential creates and updates

* feat: Display errors due to missing credentials in the correct node (#4124)

feat: Display errors due to invalid credentials in the correct node when missing permissions

* fix: remove duplicate header for coming soon features in cloud deployment

* telemetry: fix the payload for `User viewed credential tab`

* telemetry: add credential_id to 'User selected credential from node modal'

* feat: update empty states for coming soon features

* Update ActionBox.spec.ts.snap

* replace UserSharingsDetails with a subset of User properties

* rename the CreateCredentialsEditorRole to CreateCredentialsUserRole

* move IUser to the workflow package

* use IUser in the frontend as well

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Valya <68596159+valya@users.noreply.github.com>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <netroy@users.noreply.github.com>
Co-authored-by: Alex Grozav <alex@grozav.com>
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
2022-09-21 10:20:29 +02:00

1574 lines
44 KiB
Vue

<template>
<div :class="$style.container">
<n8n-callout
v-if="canPinData && hasPinData && !editMode.enabled"
theme="secondary"
icon="thumbtack"
:class="$style['pinned-data-callout']"
>
{{ $locale.baseText('runData.pindata.thisDataIsPinned') }}
<span class="ml-4xs" v-if="!isReadOnly">
<n8n-link
theme="secondary"
size="small"
underline
bold
@click="onTogglePinData({ source: 'banner-link' })"
>
{{ $locale.baseText('runData.pindata.unpin') }}
</n8n-link>
</span>
<template #trailingContent>
<n8n-link
:to="dataPinningDocsUrl"
size="small"
theme="secondary"
bold
underline
@click="onClickDataPinningDocsLink"
>
{{ $locale.baseText('runData.pindata.learnMore') }}
</n8n-link>
</template>
</n8n-callout>
<BinaryDataDisplay :windowVisible="binaryDataDisplayVisible" :displayData="binaryDataDisplayData" @close="closeBinaryDataDisplay"/>
<div :class="$style.header">
<slot name="header"></slot>
<div v-show="!hasRunError" @click.stop :class="$style.displayModes">
<n8n-radio-buttons
v-show="hasNodeRun && ((jsonData && jsonData.length > 0) || (binaryData && binaryData.length > 0)) && !editMode.enabled"
:value="displayMode"
:options="buttons"
@input="onDisplayModeChange"
/>
<n8n-icon-button
v-if="canPinData && !isReadOnly"
v-show="!editMode.enabled"
:title="$locale.baseText('runData.editOutput')"
:circle="false"
:disabled="node.disabled"
class="ml-2xs"
icon="pencil-alt"
type="tertiary"
@click="enterEditMode({ origin: 'editIconButton' })"
/>
<n8n-tooltip
placement="bottom-end"
v-if="canPinData && (jsonData && jsonData.length > 0)"
v-show="!editMode.enabled"
:value="pinDataDiscoveryTooltipVisible"
:manual="isControlledPinDataTooltip"
>
<template #content v-if="!isControlledPinDataTooltip">
<div :class="$style['tooltip-container']">
<strong>{{ $locale.baseText('ndv.pinData.pin.title') }}</strong>
<n8n-text size="small" tag="p">
{{ $locale.baseText('ndv.pinData.pin.description') }}
<n8n-link :to="dataPinningDocsUrl" size="small">
{{ $locale.baseText('ndv.pinData.pin.link') }}
</n8n-link>
</n8n-text>
</div>
</template>
<template #content v-else>
<div :class="$style['tooltip-container']">
{{ $locale.baseText('node.discovery.pinData.ndv') }}
</div>
</template>
<n8n-icon-button
:class="`ml-2xs ${$style['pin-data-button']} ${hasPinData ? $style['pin-data-button-active'] : ''}`"
type="tertiary"
active
icon="thumbtack"
:disabled="editMode.enabled || (inputData.length === 0 && !hasPinData) || isReadOnly"
@click="onTogglePinData({ source: 'pin-icon-click' })"
/>
</n8n-tooltip>
<div :class="$style['edit-mode-actions']" v-show="editMode.enabled">
<n8n-button
type="tertiary"
:label="$locale.baseText('runData.editor.cancel')"
@click="onClickCancelEdit"
/>
<n8n-button
class="ml-2xs"
type="primary"
:label="$locale.baseText('runData.editor.save')"
@click="onClickSaveEdit"
/>
</div>
</div>
</div>
<div :class="$style.runSelector" v-if="maxRunIndex > 0" v-show="!editMode.enabled">
<n8n-select size="small" :value="runIndex" @input="onRunIndexChange" @click.stop>
<template slot="prepend">{{ $locale.baseText('ndv.output.run') }}</template>
<n8n-option v-for="option in (maxRunIndex + 1)" :label="getRunLabel(option)" :value="option - 1" :key="option"></n8n-option>
</n8n-select>
<n8n-tooltip placement="right" v-if="canLinkRuns" :content="$locale.baseText(linkedRuns ? 'runData.unlinking.hint': 'runData.linking.hint')">
<n8n-icon-button v-if="linkedRuns" icon="unlink" text type="tertiary" size="small" @click="unlinkRun" />
<n8n-icon-button v-else icon="link" text type="tertiary" size="small" @click="linkRun" />
</n8n-tooltip>
<slot name="run-info"></slot>
</div>
<div v-if="maxOutputIndex > 0 && branches.length > 1" :class="{[$style.tabs]: displayMode === 'table'}">
<n8n-tabs :value="currentOutputIndex" @input="onBranchChange" :options="branches" />
</div>
<div v-else-if="hasNodeRun && dataCount > 0 && maxRunIndex === 0" v-show="!editMode.enabled" :class="$style.itemsCount">
<n8n-text>
{{ dataCount }} {{ $locale.baseText('ndv.output.items', {adjustToNumber: dataCount}) }}
</n8n-text>
</div>
<div
:class="[$style['data-container'], copyDropdownOpen ? $style['copy-dropdown-open'] : '']"
ref="dataContainer"
>
<div v-if="hasNodeRun && !hasRunError && displayMode === 'json'" v-show="!editMode.enabled" :class="$style['actions-group']">
<el-dropdown
trigger="click"
@command="handleCopyClick"
@visible-change="copyDropdownOpen = $event"
>
<span class="el-dropdown-link">
<n8n-icon-button
:title="$locale.baseText('runData.copyToClipboard')"
icon="copy"
type="tertiary"
:circle="false"
/>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{command: 'value'}">
{{ $locale.baseText('runData.copyValue') }}
</el-dropdown-item>
<el-dropdown-item :command="{command: 'itemPath'}" divided>
{{ $locale.baseText('runData.copyItemPath') }}
</el-dropdown-item>
<el-dropdown-item :command="{command: 'parameterPath'}">
{{ $locale.baseText('runData.copyParameterPath') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<div v-if="isExecuting" :class="$style.center">
<div :class="$style.spinner"><n8n-spinner type="ring" /></div>
<n8n-text>{{ executingMessage }}</n8n-text>
</div>
<div v-else-if="editMode.enabled" :class="$style['edit-mode']">
<div :class="$style['edit-mode-body']">
<code-editor
:value="editMode.value"
:options="{ scrollBeyondLastLine: false }"
type="json"
@input="$store.commit('ui/setOutputPanelEditModeValue', $event)"
/>
</div>
<div :class="$style['edit-mode-footer']">
<n8n-info-tip :bold="false" :class="$style['edit-mode-footer-infotip']">
{{ $locale.baseText('runData.editor.copyDataInfo') }}
<n8n-link :to="dataEditingDocsUrl" size="small">
{{ $locale.baseText('generic.learnMore') }}
</n8n-link>
</n8n-info-tip>
</div>
</div>
<div v-else-if="!hasNodeRun" :class="$style.center">
<slot name="node-not-run"></slot>
</div>
<div v-else-if="paneType === 'input' && node.disabled" :class="$style.center">
<n8n-text>
{{ $locale.baseText('ndv.input.disabled', { interpolate: { nodeName: node.name } }) }}
<n8n-link @click="enableNode">
{{ $locale.baseText('ndv.input.disabled.cta') }}
</n8n-link>
</n8n-text>
</div>
<div v-else-if="hasNodeRun && hasRunError" :class="$style.stretchVertically">
<n8n-text v-if="isPaneTypeInput" :class="$style.center" size="large" tag="p" bold>
{{ $locale.baseText('nodeErrorView.inputPanel.previousNodeError.title', { interpolate: { nodeName: node.name } }) }}
<n8n-link @click="goToErroredNode">
{{ $locale.baseText('nodeErrorView.inputPanel.previousNodeError.text') }}
</n8n-link>
</n8n-text>
<NodeErrorView v-else :error="workflowRunData[node.name][runIndex].error" :class="$style.errorDisplay" />
</div>
<div v-else-if="hasNodeRun && jsonData && jsonData.length === 0 && branches.length > 1" :class="$style.center">
<n8n-text>
{{ noDataInBranchMessage }}
</n8n-text>
</div>
<div v-else-if="hasNodeRun && jsonData && jsonData.length === 0" :class="$style.center">
<slot name="no-output-data"></slot>
</div>
<div v-else-if="hasNodeRun && !showData" :class="$style.center">
<n8n-text :bold="true" color="text-dark" size="large">{{ tooMuchDataTitle }}</n8n-text>
<n8n-text align="center" tag="div"><span v-html="$locale.baseText('ndv.output.tooMuchData.message', { interpolate: {size: dataSizeInMB }})"></span></n8n-text>
<n8n-button
outline
:label="$locale.baseText('ndv.output.tooMuchData.showDataAnyway')"
@click="showTooMuchData"
/>
</div>
<div v-else-if="hasNodeRun && displayMode === 'table' && binaryData.length > 0 && jsonData.length === 1 && Object.keys(jsonData[0] || {}).length === 0" :class="$style.center">
<n8n-text>
{{ $locale.baseText('runData.switchToBinary.info') }}
<a @click="switchToBinary">
{{ $locale.baseText('runData.switchToBinary.binary') }}
</a>
</n8n-text>
</div>
<div v-else-if="hasNodeRun && displayMode === 'table'" class="ph-no-capture" :class="$style.dataDisplay">
<RunDataTable :node="node" :inputData="inputData" :mappingEnabled="mappingEnabled" :distanceFromActive="distanceFromActive" :showMappingHint="showMappingHint" :runIndex="runIndex" :totalRuns="maxRunIndex" @mounted="$emit('tableMounted', $event)" />
</div>
<div v-else-if="hasNodeRun && displayMode === 'json'" class="ph-no-capture" :class="$style.jsonDisplay">
<vue-json-pretty
:data="jsonData"
:deep="10"
v-model="selectedOutput.path"
:showLine="true"
:showLength="true"
selectableType="single"
path=""
:highlightSelectedNode="true"
:selectOnClickNode="true"
@click="dataItemClicked"
class="json-data"
/>
</div>
<div v-else-if="displayMode === 'binary' && binaryData.length === 0" :class="$style.center">
<n8n-text align="center" tag="div">{{ $locale.baseText('runData.noBinaryDataFound') }}</n8n-text>
</div>
<div v-else-if="displayMode === 'binary'" :class="$style.dataDisplay">
<div v-for="(binaryDataEntry, index) in binaryData" :key="index">
<div :class="$style.binaryIndex" v-if="binaryData.length > 1">
<div>
{{index + 1}}
</div>
</div>
<div :class="$style.binaryRow">
<div :class="$style.binaryCell" v-for="(binaryData, key) in binaryDataEntry" :key="index + '_' + key">
<div>
<div :class="$style.binaryHeader">
{{key}}
</div>
<div v-if="binaryData.fileName">
<div><n8n-text size="small" :bold="true">{{ $locale.baseText('runData.fileName') }}: </n8n-text></div>
<div :class="$style.binaryValue">{{binaryData.fileName}}</div>
</div>
<div v-if="binaryData.directory">
<div><n8n-text size="small" :bold="true">{{ $locale.baseText('runData.directory') }}: </n8n-text></div>
<div :class="$style.binaryValue">{{binaryData.directory}}</div>
</div>
<div v-if="binaryData.fileExtension">
<div><n8n-text size="small" :bold="true">{{ $locale.baseText('runData.fileExtension') }}:</n8n-text></div>
<div :class="$style.binaryValue">{{binaryData.fileExtension}}</div>
</div>
<div v-if="binaryData.mimeType">
<div><n8n-text size="small" :bold="true">{{ $locale.baseText('runData.mimeType') }}: </n8n-text></div>
<div :class="$style.binaryValue">{{binaryData.mimeType}}</div>
</div>
<div :class="$style.binaryButtonContainer">
<n8n-button size="small" :label="$locale.baseText('runData.showBinaryData')" class="binary-data-show-data-button" @click="displayBinaryData(index, key)" />
<n8n-button v-if="isDownloadable(index, key)" size="small" type="secondary" :label="$locale.baseText('runData.downloadBinaryData')" class="binary-data-show-data-button" @click="downloadBinaryData(index, key)" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div :class="$style.pagination" v-if="hasNodeRun && !hasRunError && dataCount > pageSize" v-show="!editMode.enabled">
<el-pagination
background
:hide-on-single-page="true"
:current-page.sync="currentPage"
:pager-count="5"
:page-size="pageSize"
layout="prev, pager, next"
@current-change="onCurrentPageChange"
:total="dataCount">
</el-pagination>
<div :class="$style.pageSizeSelector">
<n8n-select size="mini" :value="pageSize" @input="onPageSizeChange">
<template slot="prepend">{{ $locale.baseText('ndv.output.pageSize') }}</template>
<n8n-option
v-for="size in pageSizes"
:key="size"
:label="size"
:value="size">
</n8n-option>
<n8n-option
:label="$locale.baseText('ndv.output.all')"
:value="dataCount"
>
</n8n-option>
</n8n-select>
</div>
</div>
</div>
</template>
<script lang="ts">
//@ts-ignore
import VueJsonPretty from 'vue-json-pretty';
import {
IBinaryData,
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeTypeDescription,
IRunData,
IRunExecutionData,
} from 'n8n-workflow';
import {
IBinaryDisplayData,
IExecutionResponse,
INodeUi,
IRunDataDisplayMode,
ITab,
} from '@/Interface';
import {
DATA_PINNING_DOCS_URL,
DATA_EDITING_DOCS_URL,
LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG,
LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG,
MAX_DISPLAY_DATA_SIZE,
MAX_DISPLAY_ITEMS_AUTO_ALL,
TEST_PIN_DATA,
} from '@/constants';
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
import WarningTooltip from '@/components/WarningTooltip.vue';
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
import { copyPaste } from '@/components/mixins/copyPaste';
import { externalHooks } from "@/components/mixins/externalHooks";
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { pinData } from '@/components/mixins/pinData';
import mixins from 'vue-typed-mixins';
import { saveAs } from 'file-saver';
import { CodeEditor } from "@/components/forms";
import { dataPinningEventBus } from '../event-bus/data-pinning-event-bus';
import { stringSizeInBytes } from './helpers';
import RunDataTable from './RunDataTable.vue';
import { isJsonKeyObject } from '@/utils';
// A path that does not exist so that nothing is selected by default
const deselectedPlaceholder = '_!^&*';
export type EnterEditModeArgs = {
origin: 'editIconButton' | 'insertTestDataLink',
};
export default mixins(
copyPaste,
externalHooks,
genericHelpers,
nodeHelpers,
pinData,
)
.extend({
name: 'RunData',
components: {
BinaryDataDisplay,
NodeErrorView,
VueJsonPretty,
WarningTooltip,
CodeEditor,
RunDataTable,
},
props: {
nodeUi: {
}, // INodeUi | null
runIndex: {
type: Number,
},
linkedRuns: {
type: Boolean,
},
canLinkRuns: {
type: Boolean,
},
tooMuchDataTitle: {
type: String,
},
noDataInBranchMessage: {
type: String,
},
isExecuting: {
type: Boolean,
},
executingMessage: {
type: String,
},
sessionId: {
type: String,
},
paneType: {
type: String,
},
overrideOutputs: {
type: Array,
},
mappingEnabled: {
type: Boolean,
},
distanceFromActive: {
type: Number,
},
showMappingHint: {
type: Boolean,
},
},
data () {
return {
binaryDataPreviewActive: false,
dataSize: 0,
deselectedPlaceholder,
selectedOutput: {
value: '' as object | number | string,
path: deselectedPlaceholder,
},
showData: false,
outputIndex: 0,
binaryDataDisplayVisible: false,
binaryDataDisplayData: null as IBinaryDisplayData | null,
MAX_DISPLAY_DATA_SIZE,
MAX_DISPLAY_ITEMS_AUTO_ALL,
currentPage: 1,
pageSize: 10,
pageSizes: [10, 25, 50, 100],
copyDropdownOpen: false,
eventBus: dataPinningEventBus,
pinDataDiscoveryTooltipVisible: false,
isControlledPinDataTooltip: false,
};
},
mounted() {
this.init();
if (!this.isPaneTypeInput) {
this.eventBus.$on('data-pinning-error', this.onDataPinningError);
this.eventBus.$on('data-unpinning', this.onDataUnpinning);
const hasSeenPinDataTooltip = localStorage.getItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG);
if (!hasSeenPinDataTooltip) {
this.showPinDataDiscoveryTooltip(this.jsonData);
}
}
},
destroyed() {
this.hidePinDataDiscoveryTooltip();
this.eventBus.$off('data-pinning-error', this.onDataPinningError);
this.eventBus.$off('data-unpinning', this.onDataUnpinning);
},
computed: {
activeNode(): INodeUi {
return this.$store.getters.activeNode;
},
dataPinningDocsUrl(): string {
return DATA_PINNING_DOCS_URL;
},
dataEditingDocsUrl(): string{
return DATA_EDITING_DOCS_URL;
},
displayMode(): IRunDataDisplayMode {
return this.$store.getters['ui/getPanelDisplayMode'](this.paneType);
},
node(): INodeUi | null {
return (this.nodeUi as INodeUi | null) || null;
},
nodeType (): INodeTypeDescription | null {
if (this.node) {
return this.$store.getters['nodeTypes/getNodeType'](this.node.type, this.node.typeVersion);
}
return null;
},
isTriggerNode (): boolean {
return !!(this.nodeType && this.nodeType.group.includes('trigger'));
},
canPinData (): boolean {
return !this.isPaneTypeInput &&
this.isPinDataNodeType &&
!(this.binaryData && this.binaryData.length > 0);
},
buttons(): Array<{label: string, value: string}> {
const defaults = [
{ label: this.$locale.baseText('runData.table'), value: 'table'},
{ label: this.$locale.baseText('runData.json'), value: 'json'},
];
if (this.binaryData.length) {
return [ ...defaults,
{ label: this.$locale.baseText('runData.binary'), value: 'binary'},
];
}
return defaults;
},
hasNodeRun(): boolean {
return Boolean(!this.isExecuting && this.node && (this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name) || this.hasPinData));
},
hasRunError(): boolean {
return Boolean(this.node && this.workflowRunData && this.workflowRunData[this.node.name] && this.workflowRunData[this.node.name][this.runIndex] && this.workflowRunData[this.node.name][this.runIndex].error);
},
workflowExecution (): IExecutionResponse | null {
return this.$store.getters.getWorkflowExecution;
},
workflowRunData (): IRunData | null {
if (this.workflowExecution === null) {
return null;
}
const executionData: IRunExecutionData = this.workflowExecution.data;
if (executionData && executionData.resultData) {
return executionData.resultData.runData;
}
return null;
},
dataCount (): number {
return this.getDataCount(this.runIndex, this.currentOutputIndex);
},
dataSizeInMB(): string {
return (this.dataSize / 1024 / 1000).toLocaleString();
},
maxOutputIndex (): number {
if (this.node === null) {
return 0;
}
const runData: IRunData | null = this.workflowRunData;
if (runData === null || !runData.hasOwnProperty(this.node.name)) {
return 0;
}
if (runData[this.node.name].length < this.runIndex) {
return 0;
}
if (runData[this.node.name][this.runIndex]) {
const taskData = runData[this.node.name][this.runIndex].data;
if (taskData && taskData.main) {
return taskData.main.length - 1;
}
}
return 0;
},
maxRunIndex (): number {
if (this.node === null) {
return 0;
}
const runData: IRunData | null = this.workflowRunData;
if (runData === null || !runData.hasOwnProperty(this.node.name)) {
return 0;
}
if (runData[this.node.name].length) {
return runData[this.node.name].length - 1;
}
return 0;
},
rawInputData (): INodeExecutionData[] {
let inputData: INodeExecutionData[] = [];
if (this.node) {
inputData = this.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex);
}
if (inputData.length === 0 || !Array.isArray(inputData)) {
return [];
}
return inputData;
},
inputData (): INodeExecutionData[] {
let inputData = this.rawInputData;
if (this.node && this.pinData) {
inputData = Array.isArray(this.pinData)
? this.pinData.map((value) => ({
json: value,
}))
: [{
json: this.pinData,
}];
}
const offset = this.pageSize * (this.currentPage - 1);
inputData = inputData.slice(offset, offset + this.pageSize);
return inputData;
},
jsonData (): IDataObject[] {
return this.convertToJson(this.inputData);
},
binaryData (): IBinaryKeyData[] {
if (!this.node) {
return [];
}
const binaryData = this.getBinaryData(this.workflowRunData, this.node.name, this.runIndex, this.currentOutputIndex);
return binaryData.filter((data) => Boolean(data && Object.keys(data).length));
},
currentOutputIndex(): number {
if (this.overrideOutputs && this.overrideOutputs.length && !this.overrideOutputs.includes(this.outputIndex)) {
return this.overrideOutputs[0] as number;
}
return this.outputIndex;
},
branches (): ITab[] {
function capitalize(name: string) {
return name.charAt(0).toLocaleUpperCase() + name.slice(1);
}
const branches: ITab[] = [];
for (let i = 0; i <= this.maxOutputIndex; i++) {
if (this.overrideOutputs && !this.overrideOutputs.includes(i)) {
continue;
}
const itemsCount = this.getDataCount(this.runIndex, i);
const items = this.$locale.baseText('ndv.output.items', {adjustToNumber: itemsCount});
let outputName = this.getOutputName(i);
if (`${outputName}` === `${i}`) {
outputName = `${this.$locale.baseText('ndv.output')} ${outputName}`;
}
else {
outputName = capitalize(`${this.getOutputName(i)} ${this.$locale.baseText('ndv.output.branch')}`);
}
branches.push({
label: itemsCount ? `${outputName} (${itemsCount} ${items})` : outputName,
value: i,
});
}
return branches;
},
editMode(): { enabled: boolean; value: string; } {
return this.isPaneTypeInput
? { enabled: false, value: '' }
: this.$store.getters['ui/outputPanelEditMode'];
},
isPaneTypeInput(): boolean {
return this.paneType === 'input';
},
},
methods: {
onClickDataPinningDocsLink() {
this.$telemetry.track('User clicked ndv link', {
workflow_id: this.$store.getters.workflowId,
session_id: this.sessionId,
node_type: this.activeNode.type,
pane: 'output',
type: 'data-pinning-docs',
});
},
showPinDataDiscoveryTooltip(value: IDataObject[]) {
if (!this.isTriggerNode) {
return;
}
if (value && value.length > 0) {
this.pinDataDiscoveryComplete();
setTimeout(() => {
this.isControlledPinDataTooltip = true;
this.pinDataDiscoveryTooltipVisible = true;
this.eventBus.$emit('data-pinning-discovery', { isTooltipVisible: true });
}, 500); // Wait for NDV to open
}
},
hidePinDataDiscoveryTooltip() {
if (this.pinDataDiscoveryTooltipVisible) {
this.isControlledPinDataTooltip = false;
this.pinDataDiscoveryTooltipVisible = false;
this.eventBus.$emit('data-pinning-discovery', { isTooltipVisible: false });
}
},
pinDataDiscoveryComplete() {
localStorage.setItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG, 'true');
localStorage.setItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG, 'true');
},
enterEditMode({ origin }: EnterEditModeArgs) {
const inputData = this.pinData
? this.clearJsonKey(this.pinData)
: this.convertToJson(this.rawInputData);
const data = inputData.length > 0
? inputData
: TEST_PIN_DATA;
this.$store.commit('ui/setOutputPanelEditModeEnabled', true);
this.$store.commit('ui/setOutputPanelEditModeValue', JSON.stringify(data, null, 2));
this.$telemetry.track('User opened ndv edit state', {
node_type: this.activeNode.type,
click_type: origin === 'editIconButton' ? 'button' : 'link',
session_id: this.sessionId,
run_index: this.runIndex,
is_output_present: this.hasNodeRun || this.hasPinData,
view: !this.hasNodeRun && !this.hasPinData ? 'undefined' : this.displayMode,
is_data_pinned: this.hasPinData,
});
},
onClickCancelEdit() {
this.$store.commit('ui/setOutputPanelEditModeEnabled', false);
this.$store.commit('ui/setOutputPanelEditModeValue', '');
this.onExitEditMode({ type: 'cancel' });
},
onClickSaveEdit() {
const { value } = this.editMode;
this.clearAllStickyNotifications();
if (!this.isValidPinDataSize(value)) {
this.onDataPinningError({ errorType: 'data-too-large', source: 'save-edit' });
return;
}
if (!this.isValidPinDataJSON(value)) {
this.onDataPinningError({ errorType: 'invalid-json', source: 'save-edit' });
return;
}
this.$store.commit('ui/setOutputPanelEditModeEnabled', false);
this.$store.commit('pinData', { node: this.node, data: this.clearJsonKey(value) });
this.onDataPinningSuccess({ source: 'save-edit' });
this.onExitEditMode({ type: 'save' });
},
clearJsonKey(userInput: string | object) {
const parsedUserInput = typeof userInput === 'string' ? JSON.parse(userInput) : userInput;
if (!Array.isArray(parsedUserInput)) return parsedUserInput;
return parsedUserInput.map(item => isJsonKeyObject(item) ? item.json : item);
},
onExitEditMode({ type }: { type: 'save' | 'cancel' }) {
this.$telemetry.track('User closed ndv edit state', {
node_type: this.activeNode.type,
session_id: this.sessionId,
run_index: this.runIndex,
view: this.displayMode,
type,
});
},
onDataUnpinning(
{ source }: { source: 'banner-link' | 'pin-icon-click' | 'unpin-and-execute-modal' },
) {
this.$telemetry.track('User unpinned ndv data', {
node_type: this.activeNode.type,
session_id: this.sessionId,
run_index: this.runIndex,
source,
data_size: stringSizeInBytes(this.pinData),
});
},
onDataPinningSuccess({ source }: { source: 'pin-icon-click' | 'save-edit' }) {
const telemetryPayload = {
pinning_source: source,
node_type: this.activeNode.type,
session_id: this.sessionId,
data_size: stringSizeInBytes(this.pinData),
view: this.displayMode,
run_index: this.runIndex,
};
this.$externalHooks().run('runData.onDataPinningSuccess', telemetryPayload);
this.$telemetry.track('Ndv data pinning success', telemetryPayload);
},
onDataPinningError(
{ errorType, source }: {
errorType: 'data-too-large' | 'invalid-json',
source: 'on-ndv-close-modal' | 'pin-icon-click' | 'save-edit'
},
) {
this.$telemetry.track('Ndv data pinning failure', {
pinning_source: source,
node_type: this.activeNode.type,
session_id: this.sessionId,
data_size: stringSizeInBytes(this.pinData),
view: this.displayMode,
run_index: this.runIndex,
error_type: errorType,
});
},
async onTogglePinData(
{ source }: { source: 'banner-link' | 'pin-icon-click' | 'unpin-and-execute-modal' },
) {
if (source === 'pin-icon-click') {
const telemetryPayload = {
node_type: this.activeNode.type,
session_id: this.sessionId,
run_index: this.runIndex,
view: !this.hasNodeRun && !this.hasPinData ? 'none' : this.displayMode,
};
this.$externalHooks().run('runData.onTogglePinData', telemetryPayload);
this.$telemetry.track('User clicked pin data icon', telemetryPayload);
}
this.updateNodeParameterIssues(this.node);
if (this.hasPinData) {
this.onDataUnpinning({ source });
this.$store.commit('unpinData', { node: this.node });
return;
}
const data = this.convertToJson(this.rawInputData);
if (!this.isValidPinDataSize(data)) {
this.onDataPinningError({ errorType: 'data-too-large', source: 'pin-icon-click' });
return;
}
this.onDataPinningSuccess({ source: 'save-edit' });
this.$store.commit('pinData', { node: this.node, data });
if (this.maxRunIndex > 0) {
this.$showToast({
title: this.$locale.baseText('ndv.pinData.pin.multipleRuns.title', {
interpolate: {
index: `${this.runIndex}`,
},
}),
message: this.$locale.baseText('ndv.pinData.pin.multipleRuns.description'),
type: 'success',
duration: 2000,
});
}
this.hidePinDataDiscoveryTooltip();
this.pinDataDiscoveryComplete();
},
switchToBinary() {
this.onDisplayModeChange('binary');
},
onBranchChange(value: number) {
this.outputIndex = value;
this.$telemetry.track('User changed ndv branch', {
session_id: this.sessionId,
branch_index: value,
node_type: this.activeNode.type,
node_type_input_selection: this.nodeType? this.nodeType.name: '',
pane: this.paneType,
});
},
showTooMuchData() {
this.showData = true;
this.$telemetry.track('User clicked ndv button', {
node_type: this.activeNode.type,
workflow_id: this.$store.getters.workflowId,
session_id: this.sessionId,
pane: this.paneType,
type: 'showTooMuchData',
});
},
linkRun() {
this.$emit('linkRun');
},
unlinkRun() {
this.$emit('unlinkRun');
},
onCurrentPageChange() {
this.$telemetry.track('User changed ndv page', {
node_type: this.activeNode.type,
workflow_id: this.$store.getters.workflowId,
session_id: this.sessionId,
pane: this.paneType,
page_selected: this.currentPage,
page_size: this.pageSize,
items_total: this.dataCount,
});
},
onPageSizeChange(pageSize: number) {
this.pageSize = pageSize;
const maxPage = Math.ceil(this.dataCount / this.pageSize);
if (maxPage < this.currentPage) {
this.currentPage = maxPage;
}
this.$telemetry.track('User changed ndv page size', {
node_type: this.activeNode.type,
workflow_id: this.$store.getters.workflowId,
session_id: this.sessionId,
pane: this.paneType,
page_selected: this.currentPage,
page_size: this.pageSize,
items_total: this.dataCount,
});
},
onDisplayModeChange(displayMode: IRunDataDisplayMode) {
const previous = this.displayMode;
this.$store.commit('ui/setPanelDisplayMode', {pane: this.paneType, mode: displayMode});
const dataContainer = this.$refs.dataContainer;
if (dataContainer) {
const dataDisplay = (dataContainer as Element).children[0];
if (dataDisplay){
dataDisplay.scrollTo(0, 0);
}
}
this.closeBinaryDataDisplay();
this.$externalHooks().run('runData.displayModeChanged', { newValue: displayMode, oldValue: previous });
if(this.activeNode) {
this.$telemetry.track('User changed ndv item view', {
previous_view: previous,
new_view: displayMode,
node_type: this.activeNode.type,
workflow_id: this.$store.getters.workflowId,
session_id: this.sessionId,
pane: this.paneType,
});
}
},
getRunLabel(option: number) {
let itemsCount = 0;
for (let i = 0; i <= this.maxOutputIndex; i++) {
itemsCount += this.getDataCount(option - 1, i);
}
const items = this.$locale.baseText('ndv.output.items', {adjustToNumber: itemsCount});
const itemsLabel = itemsCount > 0 ? ` (${itemsCount} ${items})` : '';
return option + this.$locale.baseText('ndv.output.of') + (this.maxRunIndex+1) + itemsLabel;
},
getDataCount(runIndex: number, outputIndex: number) {
if (this.node === null) {
return 0;
}
const runData: IRunData | null = this.workflowRunData;
if (runData === null || !runData.hasOwnProperty(this.node.name)) {
return 0;
}
if (runData[this.node.name].length <= runIndex) {
return 0;
}
if (runData[this.node.name][runIndex].hasOwnProperty('error')) {
return 1;
}
if (!runData[this.node.name][runIndex].hasOwnProperty('data') ||
runData[this.node.name][runIndex].data === undefined
) {
return 0;
}
const inputData = this.getMainInputData(runData[this.node.name][runIndex].data!, outputIndex);
return inputData.length;
},
init() {
// Reset the selected output index every time another node gets selected
this.outputIndex = 0;
this.refreshDataSize();
this.closeBinaryDataDisplay();
if (this.binaryData.length > 0) {
this.$store.commit('ui/setPanelDisplayMode', {pane: this.paneType, mode: 'binary'});
}
else if (this.displayMode === 'binary') {
this.$store.commit('ui/setPanelDisplayMode', {pane: this.paneType, mode: 'table'});
}
},
closeBinaryDataDisplay () {
this.binaryDataDisplayVisible = false;
this.binaryDataDisplayData = null;
},
convertToJson (inputData: INodeExecutionData[]): IDataObject[] {
const returnData: IDataObject[] = [];
inputData.forEach((data) => {
if (!data.hasOwnProperty('json')) {
return;
}
returnData.push(data.json);
});
return returnData;
},
clearExecutionData () {
this.$store.commit('setWorkflowExecutionData', null);
this.updateNodesExecutionIssues();
},
dataItemClicked (path: string, data: object | number | string) {
this.selectedOutput.value = data;
},
isDownloadable (index: number, key: string): boolean {
const binaryDataItem: IBinaryData = this.binaryData[index][key];
return !!(binaryDataItem.mimeType && binaryDataItem.fileName);
},
async downloadBinaryData (index: number, key: string) {
const binaryDataItem: IBinaryData = this.binaryData[index][key];
let bufferString = 'data:' + binaryDataItem.mimeType + ';base64,';
if(binaryDataItem.id) {
bufferString += await this.restApi().getBinaryBufferString(binaryDataItem.id);
} else {
bufferString += binaryDataItem.data;
}
const data = await fetch(bufferString);
const blob = await data.blob();
saveAs(blob, binaryDataItem.fileName);
},
displayBinaryData (index: number, key: string) {
this.binaryDataDisplayVisible = true;
this.binaryDataDisplayData = {
node: this.node!.name,
runIndex: this.runIndex,
outputIndex: this.currentOutputIndex,
index,
key,
};
},
getOutputName (outputIndex: number) {
if (this.node === null) {
return outputIndex + 1;
}
const nodeType = this.nodeType;
if (!nodeType || !nodeType.outputNames || nodeType.outputNames.length <= outputIndex) {
return outputIndex + 1;
}
return nodeType.outputNames[outputIndex];
},
convertPath (path: string): string {
// TODO: That can for sure be done fancier but for now it works
const placeholder = '*___~#^#~___*';
let inBrackets = path.match(/\[(.*?)\]/g);
if (inBrackets === null) {
inBrackets = [];
} else {
inBrackets = inBrackets.map(item => item.slice(1, -1)).map(item => {
if (item.startsWith('"') && item.endsWith('"')) {
return item.slice(1, -1);
}
return item;
});
}
const withoutBrackets = path.replace(/\[(.*?)\]/g, placeholder);
const pathParts = withoutBrackets.split('.');
const allParts = [] as string[];
pathParts.forEach(part => {
let index = part.indexOf(placeholder);
while(index !== -1) {
if (index === 0) {
allParts.push(inBrackets!.shift() as string);
part = part.substr(placeholder.length);
} else {
allParts.push(part.substr(0, index));
part = part.substr(index);
}
index = part.indexOf(placeholder);
}
if (part !== '') {
allParts.push(part);
}
});
return '["' + allParts.join('"]["') + '"]';
},
handleCopyClick (commandData: { command: string }) {
const isNotSelected = this.selectedOutput.path === deselectedPlaceholder;
const selectedPath = isNotSelected ? '[""]' : this.selectedOutput.path;
let selectedValue = this.selectedOutput.value;
if (isNotSelected) {
if (this.hasPinData) {
selectedValue = this.clearJsonKey(this.pinData as object);
} else {
selectedValue = this.convertToJson(this.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex));
}
}
const newPath = this.convertPath(selectedPath);
let value: string;
if (commandData.command === 'value') {
if (typeof selectedValue === 'object') {
value = JSON.stringify(selectedValue, null, 2);
} else {
value = selectedValue.toString();
}
this.$showToast({
title: this.$locale.baseText('runData.copyValue.toast'),
message: '',
type: 'success',
duration: 2000,
});
} else {
let startPath = '';
let path = '';
if (commandData.command === 'itemPath') {
const pathParts = newPath.split(']');
const index = pathParts[0].slice(1);
path = pathParts.slice(1).join(']');
startPath = `$item(${index}).$node["${this.node!.name}"].json`;
this.$showToast({
title: this.$locale.baseText('runData.copyItemPath.toast'),
message: '',
type: 'success',
duration: 2000,
});
} else if (commandData.command === 'parameterPath') {
path = newPath.split(']').slice(1).join(']');
startPath = `$node["${this.node!.name}"].json`;
this.$showToast({
title: this.$locale.baseText('runData.copyParameterPath.toast'),
message: '',
type: 'success',
duration: 2000,
});
}
if (!path.startsWith('[') && !path.startsWith('.') && path) {
path += '.';
}
value = `{{ ${startPath + path} }}`;
}
const copyType = {
value: 'selection',
itemPath: 'item_path',
parameterPath: 'parameter_path',
}[commandData.command];
this.$telemetry.track('User copied ndv data', {
node_type: this.activeNode.type,
session_id: this.sessionId,
run_index: this.runIndex,
view: this.displayMode,
copy_type: copyType,
workflow_id: this.$store.getters.workflowId,
pane: 'output',
in_execution_log: this.isReadOnly,
});
this.copyToClipboard(value);
},
refreshDataSize () {
// Hide by default the data from being displayed
this.showData = false;
// Check how much data there is to display
const inputData = this.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex);
const offset = this.pageSize * (this.currentPage - 1);
const jsonItems = inputData.slice(offset, offset + this.pageSize).map(item => item.json);
this.dataSize = JSON.stringify(jsonItems).length;
if (this.dataSize < this.MAX_DISPLAY_DATA_SIZE) {
// Data is reasonable small (< 200kb) so display it directly
this.showData = true;
}
},
onRunIndexChange(run: number) {
this.$emit('runChange', run);
},
enableNode() {
if (this.node) {
const updateInformation = {
name: this.node.name,
properties: {
disabled: !this.node.disabled,
},
};
this.$store.commit('updateNodeProperties', updateInformation);
}
},
goToErroredNode() {
if (this.node) {
this.$store.commit('setActiveNode', this.node.name);
}
},
},
watch: {
node() {
this.init();
},
jsonData (value: IDataObject[]) {
this.refreshDataSize();
const hasSeenPinDataTooltip = localStorage.getItem(LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG);
if (!hasSeenPinDataTooltip) {
this.showPinDataDiscoveryTooltip(value);
}
},
binaryData (newData: IBinaryKeyData[], prevData: IBinaryKeyData[]) {
if (newData.length && !prevData.length && this.displayMode !== 'binary') {
this.switchToBinary();
}
else if (!newData.length && this.displayMode === 'binary') {
this.onDisplayModeChange('table');
}
},
},
});
</script>
<style lang="scss" module>
.infoIcon {
color: var(--color-foreground-dark);
}
.center {
display: flex;
height: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-s) var(--spacing-s) var(--spacing-xl) var(--spacing-s);
text-align: center;
> * {
max-width: 316px;
margin-bottom: var(--spacing-2xs);
}
}
.container {
position: relative;
width: 100%;
height: 100%;
background-color: var(--color-background-base);
display: flex;
flex-direction: column;
}
.pinned-data-callout {
border-radius: inherit;
border-bottom-right-radius: 0;
border-top: 0;
border-left: 0;
border-right: 0;
}
.header {
display: flex;
align-items: center;
margin-bottom: var(--spacing-s);
padding: var(--spacing-s) var(--spacing-s) 0 var(--spacing-s);
position: relative;
> *:first-child {
flex-grow: 1;
}
}
.data-container {
position: relative;
height: 100%;
&:hover,
&.copy-dropdown-open {
.actions-group {
opacity: 1;
}
}
}
.dataDisplay {
position: absolute;
top: 0;
left: 0;
padding-left: var(--spacing-s);
right: 0;
overflow-y: auto;
line-height: 1.5;
word-break: normal;
height: 100%;
padding-bottom: var(--spacing-3xl);
}
.errorDisplay {
composes: dataDisplay;
padding-right: var(--spacing-s);
}
.jsonDisplay {
composes: dataDisplay;
background-color: var(--color-background-base);
padding-top: var(--spacing-s);
}
.tabs {
margin-bottom: var(--spacing-s);
}
.itemsCount {
margin-left: var(--spacing-s);
margin-bottom: var(--spacing-s);
}
.runSelector {
max-width: 210px;
margin-left: var(--spacing-s);
margin-bottom: var(--spacing-s);
display: flex;
align-items: center;
> * {
margin-right: var(--spacing-4xs);
}
}
.actions-group {
position: absolute;
z-index: 10;
top: 12px;
right: var(--spacing-l);
opacity: 0;
transition: opacity 0.3s ease;
}
.pagination {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
bottom: 0;
padding: 5px;
}
.binaryIndex {
display: block;
padding: var(--spacing-2xs);
font-size: var(--font-size-2xs);
> * {
display: inline-block;
width: 30px;
height: 30px;
line-height: 30px;
border-radius: var(--border-radius-base);
text-align: center;
background-color: var(--color-foreground-xdark);
font-weight: var(--font-weight-bold);
color: var(--color-text-xlight);
}
}
.binaryRow {
display: inline-flex;
font-size: var(--font-size-2xs);
}
.binaryCell {
display: inline-block;
width: 300px;
overflow: hidden;
background-color: var(--color-foreground-xlight);
margin-right: var(--spacing-s);
margin-bottom: var(--spacing-s);
border-radius: var(--border-radius-base);
border: var(--border-base);
padding: var(--spacing-s);
}
.binaryHeader {
color: $--color-primary;
font-weight: 600;
font-size: 1.2em;
padding-bottom: 0.5em;
margin-bottom: 0.5em;
border-bottom: 1px solid var(--color-text-light);
}
.binaryButtonContainer {
margin-top: 1.5em;
display: flex;
flex-direction: row;
justify-content: center;
> * {
flex-grow: 0;
margin-right: var(--spacing-3xs);
}
}
.binaryValue {
white-space: initial;
word-wrap: break-word;
}
.pageSizeSelector {
text-transform: capitalize;
max-width: 150px;
}
.displayModes {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
.tooltip-container {
max-width: 240px;
}
.pin-data-button {
svg {
transition: transform 0.3s ease;
}
}
.pin-data-button-active {
&,
&:hover,
&:focus,
&:active {
border-color: var(--color-primary);
color: var(--color-primary);
background: var(--color-primary-tint-2);
}
}
.spinner {
* {
color: var(--color-primary);
min-height: 40px;
min-width: 40px;
}
display: flex;
justify-content: center;
margin-bottom: var(--spacing-s);
}
.edit-mode {
height: calc(100% - var(--spacing-s));
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
padding-left: var(--spacing-s);
padding-right: var(--spacing-s);
}
.edit-mode-body {
flex: 1 1 auto;
width: 100%;
height: 100%;
overflow: hidden;
}
.edit-mode-footer {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding-top: var(--spacing-s);
}
.edit-mode-footer-infotip {
display: flex;
flex: 1;
width: 100%;
}
.edit-mode-actions {
display: flex;
justify-content: flex-end;
align-items: center;
margin-left: var(--spacing-s);
}
.stretchVertically {
height: 100%;
}
</style>
<style lang="scss">
.vjs-tree {
color: var(--color-json-default);
}
.vjs-tree.is-highlight-selected {
background-color: var(--color-json-highlight);
}
.vjs-tree .vjs-value__null {
color: var(--color-json-null);
}
.vjs-tree .vjs-value__boolean {
color: var(--color-json-boolean);
}
.vjs-tree .vjs-value__number {
color: var(--color-json-number);
}
.vjs-tree .vjs-value__string {
color: var(--color-json-string);
}
.vjs-tree .vjs-key {
color: var(--color-json-key);
}
.vjs-tree .vjs-tree__brackets {
color: var(--color-json-brackets);
}
.vjs-tree .vjs-tree__brackets:hover {
color: var(--color-json-brackets-hover);
}
.vjs-tree .vjs-tree__content.has-line {
border-left: 1px dotted var(--color-json-line);
}
</style>