mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
feat(editor): Show locked state and permission notice on data redaction workflow settings (#30022)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
parent
0d571c05e4
commit
7635131bd3
|
|
@ -3816,6 +3816,10 @@
|
|||
"workflowSettings.redactionOptions.redact": "Redact",
|
||||
"workflowSettings.helpTexts.redactProductionData": "Controls whether execution data from production (non-manually triggered) executions is redacted.",
|
||||
"workflowSettings.helpTexts.redactManualData": "Controls whether execution data from manually triggered executions is redacted.",
|
||||
"workflowSettings.redactionPermissionNotice": "You don't have permission to change data redaction settings.",
|
||||
"workflowSettings.redactionPermissionNotice.viewUsers": "View users with access",
|
||||
"workflowSettings.redactionMembersModal.title": "Users who can edit data redaction settings",
|
||||
"workflowSettings.redactionMembersModal.description": "These users can change data redaction settings. Contact one of them to request a change.",
|
||||
"workflowSettings.saveManualExecutions": "Save manual executions",
|
||||
"workflowSettings.saveManualOptions.defaultSave": "Default - {defaultValue}",
|
||||
"workflowSettings.saveManualOptions.doNotSave": "Do not save",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
<script setup lang="ts">
|
||||
import { useRolesStore } from '@/app/stores/roles.store';
|
||||
import { N8nDialog, N8nIconButton, N8nLoading, N8nText, N8nUserInfo } from '@n8n/design-system';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import type { RoleProjectMembersResponse } from '@n8n/api-types';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
projectId: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:open': [value: boolean];
|
||||
}>();
|
||||
|
||||
const rolesStore = useRolesStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const membersData = ref<RoleProjectMembersResponse>({ members: [] });
|
||||
const isLoading = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
async (isOpen) => {
|
||||
if (!isOpen) return;
|
||||
|
||||
isLoading.value = true;
|
||||
try {
|
||||
membersData.value = await rolesStore.fetchRoleProjectMembers(
|
||||
'project:admin',
|
||||
props.projectId,
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nDialog
|
||||
:open="open"
|
||||
size="medium"
|
||||
:show-close-button="false"
|
||||
@update:open="emit('update:open', $event)"
|
||||
>
|
||||
<div :class="$style.header">
|
||||
<N8nIconButton
|
||||
icon="chevron-left"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
:aria-label="i18n.baseText('generic.back')"
|
||||
@click="emit('update:open', false)"
|
||||
/>
|
||||
<N8nText tag="h2" size="large" bold>
|
||||
{{ i18n.baseText('workflowSettings.redactionMembersModal.title') }}
|
||||
</N8nText>
|
||||
</div>
|
||||
|
||||
<div :class="$style.content">
|
||||
<N8nText color="text-base" size="small" :class="$style.description">
|
||||
{{ i18n.baseText('workflowSettings.redactionMembersModal.description') }}
|
||||
</N8nText>
|
||||
<N8nLoading v-if="isLoading" :rows="3" />
|
||||
<div v-else :class="$style.memberList">
|
||||
<div v-for="member in membersData.members" :key="member.userId" :class="$style.memberRow">
|
||||
<N8nUserInfo
|
||||
:first-name="member.firstName"
|
||||
:last-name="member.lastName"
|
||||
:email="member.email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</N8nDialog>
|
||||
</template>
|
||||
|
||||
<style lang="css" module>
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing--xs);
|
||||
padding-bottom: var(--spacing--xs);
|
||||
}
|
||||
|
||||
.content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: block;
|
||||
margin-bottom: var(--spacing--sm);
|
||||
}
|
||||
|
||||
.memberList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.memberRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing--xs) 0;
|
||||
border-bottom: var(--border);
|
||||
}
|
||||
|
||||
.memberRow:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -968,11 +968,11 @@ describe('WorkflowSettingsVue', () => {
|
|||
});
|
||||
|
||||
describe('Redaction Policy', () => {
|
||||
it('should not render redaction policy when env feature flag is missing', async () => {
|
||||
const { queryByTestId } = createComponent({ pinia });
|
||||
it('should show locked redaction policy when licensed but user lacks updateRedactionSetting scope', async () => {
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
expect(queryByTestId('workflow-settings-redaction-policy')).not.toBeInTheDocument();
|
||||
expect(getByTestId('workflow-settings-redaction-policy')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render redaction policy when redaction module is inactive', async () => {
|
||||
|
|
@ -995,13 +995,20 @@ describe('WorkflowSettingsVue', () => {
|
|||
expect(queryByTestId('workflow-settings-redaction-policy')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render redaction policy when user lacks updateRedactionSetting scope', async () => {
|
||||
it('should disable redaction dropdowns when user lacks updateRedactionSetting scope', async () => {
|
||||
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
|
||||
|
||||
const { queryByTestId } = createComponent({ pinia });
|
||||
const { getByTestId } = createComponent({ pinia });
|
||||
await flushPromises();
|
||||
|
||||
expect(queryByTestId('workflow-settings-redaction-policy')).not.toBeInTheDocument();
|
||||
const productionCombobox = within(
|
||||
getByTestId('workflow-settings-redact-production-select'),
|
||||
).getByRole('combobox');
|
||||
const manualCombobox = within(
|
||||
getByTestId('workflow-settings-redact-manual-select'),
|
||||
).getByRole('combobox');
|
||||
expect(productionCombobox).toBeDisabled();
|
||||
expect(manualCombobox).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should render redaction policy when module is active and user has scope', async () => {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import { useTelemetry } from '@/app/composables/useTelemetry';
|
|||
import { useDebounce } from '@/app/composables/useDebounce';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
|
||||
import RedactionMembersModal from '@/app/components/RedactionMembersModal.vue';
|
||||
import { useGlobalLinkActions } from '@/app/composables/useGlobalLinkActions';
|
||||
import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper';
|
||||
import { useNodeCreatorStore } from '@/features/shared/nodeCreator/nodeCreator.store';
|
||||
|
|
@ -202,12 +203,14 @@ const isDataRedactionLicensed = computed(
|
|||
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.DataRedaction],
|
||||
);
|
||||
|
||||
const isRedactionSettingVisible = computed(
|
||||
() =>
|
||||
settingsStore.isModuleActive('redaction') &&
|
||||
(isDataRedactionLicensed.value ? workflowPermissions.value.updateRedactionSetting : true),
|
||||
const isRedactionSettingVisible = computed(() => settingsStore.isModuleActive('redaction'));
|
||||
|
||||
const isRedactionSettingLocked = computed(
|
||||
() => isDataRedactionLicensed.value && !workflowPermissions.value.updateRedactionSetting,
|
||||
);
|
||||
|
||||
const redactionMembersModalOpen = ref(false);
|
||||
|
||||
function goToDataRedactionUpgrade() {
|
||||
void pageRedirectionHelper.goToUpgrade('workflow-settings', 'upgrade-data-redaction');
|
||||
}
|
||||
|
|
@ -1171,10 +1174,19 @@ onBeforeUnmount(() => {
|
|||
:span="10"
|
||||
:class="[
|
||||
$style['setting-name'],
|
||||
{ [$style['setting-name--disabled']]: !isDataRedactionLicensed },
|
||||
{
|
||||
[$style['setting-name--disabled']]:
|
||||
!isDataRedactionLicensed || isRedactionSettingLocked,
|
||||
},
|
||||
]"
|
||||
>
|
||||
{{ i18n.baseText('workflowSettings.redactProductionData') }}
|
||||
<N8nIcon
|
||||
v-if="isRedactionSettingLocked"
|
||||
icon="lock"
|
||||
size="xsmall"
|
||||
style="opacity: 1"
|
||||
/>
|
||||
<N8nBadge
|
||||
v-if="!isDataRedactionLicensed"
|
||||
:class="[$style['upgrade-badge'], 'ml-4xs']"
|
||||
|
|
@ -1189,13 +1201,31 @@ onBeforeUnmount(() => {
|
|||
<N8nIcon icon="circle-help" />
|
||||
</N8nTooltip>
|
||||
</ElCol>
|
||||
<ElCol :span="14" class="ignore-key-press-canvas">
|
||||
<ElCol
|
||||
:span="14"
|
||||
class="ignore-key-press-canvas"
|
||||
:class="{ [$style['setting-name--disabled']]: isRedactionSettingLocked }"
|
||||
>
|
||||
<N8nTooltip :disabled="!isRedactionSettingLocked" :enterable="true" placement="top">
|
||||
<template #content>
|
||||
<span
|
||||
>{{ i18n.baseText('workflowSettings.redactionPermissionNotice') }}
|
||||
<span
|
||||
:class="$style['permission-notice-link']"
|
||||
@click="redactionMembersModalOpen = true"
|
||||
>{{
|
||||
i18n.baseText('workflowSettings.redactionPermissionNotice.viewUsers')
|
||||
}}</span
|
||||
></span
|
||||
>
|
||||
</template>
|
||||
<N8nSelect
|
||||
v-model="redactProductionData"
|
||||
:disabled="
|
||||
!isDataRedactionLicensed ||
|
||||
readOnlyEnv ||
|
||||
!workflowPermissions.updateRedactionSetting ||
|
||||
isRedactionSettingLocked ||
|
||||
workflowHasDynamicCredentials
|
||||
"
|
||||
:placeholder="i18n.baseText('workflowSettings.selectOption')"
|
||||
|
|
@ -1211,6 +1241,7 @@ onBeforeUnmount(() => {
|
|||
>
|
||||
</N8nOption>
|
||||
</N8nSelect>
|
||||
</N8nTooltip>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow v-if="workflowHasDynamicCredentials" :class="$style['dynamic-credentials-hint']">
|
||||
|
|
@ -1226,10 +1257,14 @@ onBeforeUnmount(() => {
|
|||
:span="10"
|
||||
:class="[
|
||||
$style['setting-name'],
|
||||
{ [$style['setting-name--disabled']]: !isDataRedactionLicensed },
|
||||
{
|
||||
[$style['setting-name--disabled']]:
|
||||
!isDataRedactionLicensed || isRedactionSettingLocked,
|
||||
},
|
||||
]"
|
||||
>
|
||||
{{ i18n.baseText('workflowSettings.redactManualData') }}
|
||||
<N8nIcon v-if="isRedactionSettingLocked" icon="lock" size="xsmall" />
|
||||
<N8nBadge
|
||||
v-if="!isDataRedactionLicensed"
|
||||
:class="[$style['upgrade-badge'], 'ml-4xs']"
|
||||
|
|
@ -1244,13 +1279,31 @@ onBeforeUnmount(() => {
|
|||
<N8nIcon icon="circle-help" />
|
||||
</N8nTooltip>
|
||||
</ElCol>
|
||||
<ElCol :span="14" class="ignore-key-press-canvas">
|
||||
<ElCol
|
||||
:span="14"
|
||||
class="ignore-key-press-canvas"
|
||||
:class="{ [$style['setting-name--disabled']]: isRedactionSettingLocked }"
|
||||
>
|
||||
<N8nTooltip :disabled="!isRedactionSettingLocked" :enterable="true" placement="top">
|
||||
<template #content>
|
||||
<span
|
||||
>{{ i18n.baseText('workflowSettings.redactionPermissionNotice') }}
|
||||
<span
|
||||
:class="$style['permission-notice-link']"
|
||||
@click="redactionMembersModalOpen = true"
|
||||
>{{
|
||||
i18n.baseText('workflowSettings.redactionPermissionNotice.viewUsers')
|
||||
}}</span
|
||||
></span
|
||||
>
|
||||
</template>
|
||||
<N8nSelect
|
||||
v-model="redactManualData"
|
||||
:disabled="
|
||||
!isDataRedactionLicensed ||
|
||||
readOnlyEnv ||
|
||||
!workflowPermissions.updateRedactionSetting
|
||||
!workflowPermissions.updateRedactionSetting ||
|
||||
isRedactionSettingLocked
|
||||
"
|
||||
:placeholder="i18n.baseText('workflowSettings.selectOption')"
|
||||
filterable
|
||||
|
|
@ -1265,6 +1318,7 @@ onBeforeUnmount(() => {
|
|||
>
|
||||
</N8nOption>
|
||||
</N8nSelect>
|
||||
</N8nTooltip>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</template>
|
||||
|
|
@ -1494,6 +1548,12 @@ onBeforeUnmount(() => {
|
|||
@click="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
<RedactionMembersModal
|
||||
v-if="workflowDocumentStore?.homeProject?.id"
|
||||
:open="redactionMembersModalOpen"
|
||||
:project-id="workflowDocumentStore?.homeProject?.id"
|
||||
@update:open="redactionMembersModalOpen = $event"
|
||||
/>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -1545,6 +1605,12 @@ onBeforeUnmount(() => {
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.permission-notice-link {
|
||||
color: var(--color-foreground-xlight);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upgrade-badge {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user