mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix(editor): Follow up fixes and improvements to viewer role (#10684)
This commit is contained in:
parent
efa5573278
commit
63548e6ead
|
@ -85,4 +85,10 @@ describe('CredentialCard', () => {
|
||||||
}
|
}
|
||||||
expect(actions).toHaveTextContent('Move');
|
expect(actions).toHaveTextContent('Move');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set readOnly variant based on prop', () => {
|
||||||
|
const { getByRole } = renderComponent({ props: { readOnly: true } });
|
||||||
|
const heading = getByRole('heading');
|
||||||
|
expect(heading).toHaveTextContent('Read only');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -138,16 +138,19 @@ function moveResource() {
|
||||||
<template #header>
|
<template #header>
|
||||||
<n8n-heading tag="h2" bold :class="$style.cardHeading">
|
<n8n-heading tag="h2" bold :class="$style.cardHeading">
|
||||||
{{ data.name }}
|
{{ data.name }}
|
||||||
|
<N8nBadge v-if="readOnly" class="ml-3xs" theme="tertiary" bold>
|
||||||
|
{{ locale.baseText('credentials.item.readonly') }}
|
||||||
|
</N8nBadge>
|
||||||
</n8n-heading>
|
</n8n-heading>
|
||||||
</template>
|
</template>
|
||||||
<div :class="$style.cardDescription">
|
<div :class="$style.cardDescription">
|
||||||
<n8n-text color="text-light" size="small">
|
<n8n-text color="text-light" size="small">
|
||||||
<span v-if="credentialType">{{ credentialType.displayName }} | </span>
|
<span v-if="credentialType">{{ credentialType.displayName }} | </span>
|
||||||
<span v-show="data"
|
<span v-show="data"
|
||||||
>{{ $locale.baseText('credentials.item.updated') }} <TimeAgo :date="data.updatedAt" /> |
|
>{{ locale.baseText('credentials.item.updated') }} <TimeAgo :date="data.updatedAt" /> |
|
||||||
</span>
|
</span>
|
||||||
<span v-show="data"
|
<span v-show="data"
|
||||||
>{{ $locale.baseText('credentials.item.created') }} {{ formattedCreatedAtDate }}
|
>{{ locale.baseText('credentials.item.created') }} {{ formattedCreatedAtDate }}
|
||||||
</span>
|
</span>
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -407,7 +407,7 @@ async function beforeClose() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
keepEditing = confirmAction === MODAL_CONFIRM;
|
keepEditing = confirmAction === MODAL_CONFIRM;
|
||||||
} else if (isOAuthType.value && !isOAuthConnected.value) {
|
} else if (credentialPermissions.value.update && isOAuthType.value && !isOAuthConnected.value) {
|
||||||
const confirmAction = await message.confirm(
|
const confirmAction = await message.confirm(
|
||||||
i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.message'),
|
i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.message'),
|
||||||
i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.headline'),
|
i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.headline'),
|
||||||
|
|
|
@ -57,7 +57,7 @@ describe('WorkflowCard', () => {
|
||||||
it('should render a card with the workflow name and open workflow clicking on it', async () => {
|
it('should render a card with the workflow name and open workflow clicking on it', async () => {
|
||||||
const data = createWorkflow();
|
const data = createWorkflow();
|
||||||
const { getByRole } = renderComponent({ props: { data } });
|
const { getByRole } = renderComponent({ props: { data } });
|
||||||
const cardTitle = getByRole('heading', { level: 2, name: data.name });
|
const cardTitle = getByRole('heading', { level: 2, name: new RegExp(data.name) });
|
||||||
|
|
||||||
expect(cardTitle).toBeInTheDocument();
|
expect(cardTitle).toBeInTheDocument();
|
||||||
|
|
||||||
|
@ -166,4 +166,12 @@ describe('WorkflowCard', () => {
|
||||||
}
|
}
|
||||||
expect(actions).toHaveTextContent('Move');
|
expect(actions).toHaveTextContent('Move');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show Read only mode', async () => {
|
||||||
|
const data = createWorkflow();
|
||||||
|
const { getByRole } = renderComponent({ props: { data } });
|
||||||
|
|
||||||
|
const heading = getByRole('heading');
|
||||||
|
expect(heading).toHaveTextContent('Read only');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -236,16 +236,19 @@ function moveResource() {
|
||||||
<template #header>
|
<template #header>
|
||||||
<n8n-heading tag="h2" bold :class="$style.cardHeading" data-test-id="workflow-card-name">
|
<n8n-heading tag="h2" bold :class="$style.cardHeading" data-test-id="workflow-card-name">
|
||||||
{{ data.name }}
|
{{ data.name }}
|
||||||
|
<N8nBadge v-if="!workflowPermissions.update" class="ml-3xs" theme="tertiary" bold>
|
||||||
|
{{ locale.baseText('workflows.item.readonly') }}
|
||||||
|
</N8nBadge>
|
||||||
</n8n-heading>
|
</n8n-heading>
|
||||||
</template>
|
</template>
|
||||||
<div :class="$style.cardDescription">
|
<div :class="$style.cardDescription">
|
||||||
<n8n-text color="text-light" size="small">
|
<n8n-text color="text-light" size="small">
|
||||||
<span v-show="data"
|
<span v-show="data"
|
||||||
>{{ $locale.baseText('workflows.item.updated') }}
|
>{{ locale.baseText('workflows.item.updated') }}
|
||||||
<TimeAgo :date="String(data.updatedAt)" /> |
|
<TimeAgo :date="String(data.updatedAt)" /> |
|
||||||
</span>
|
</span>
|
||||||
<span v-show="data" class="mr-2xs"
|
<span v-show="data" class="mr-2xs"
|
||||||
>{{ $locale.baseText('workflows.item.created') }} {{ formattedCreatedAtDate }}
|
>{{ locale.baseText('workflows.item.created') }} {{ formattedCreatedAtDate }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="settingsStore.areTagsEnabled && data.tags && data.tags.length > 0"
|
v-if="settingsStore.areTagsEnabled && data.tags && data.tags.length > 0"
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import { describe, expect } from 'vitest';
|
import { describe, expect } from 'vitest';
|
||||||
import { render } from '@testing-library/vue';
|
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory, RouterLink } from 'vue-router';
|
||||||
import { createPinia, PiniaVuePlugin, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import { randomInt, type ExecutionSummary } from 'n8n-workflow';
|
import { randomInt, type ExecutionSummary } from 'n8n-workflow';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import WorkflowExecutionsPreview from '@/components/executions/workflow/WorkflowExecutionsPreview.vue';
|
import WorkflowExecutionsPreview from '@/components/executions/workflow/WorkflowExecutionsPreview.vue';
|
||||||
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
import { EnterpriseEditionFeature, VIEWS } from '@/constants';
|
||||||
import { i18nInstance, I18nPlugin } from '@/plugins/i18n';
|
|
||||||
import { FontAwesomePlugin } from '@/plugins/icons';
|
|
||||||
import { GlobalComponentsPlugin } from '@/plugins/components';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type { ExecutionSummaryWithScopes, IWorkflowDb } from '@/Interface';
|
import type { ExecutionSummaryWithScopes, IWorkflowDb } from '@/Interface';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
|
||||||
let pinia: ReturnType<typeof createPinia>;
|
let pinia: ReturnType<typeof createPinia>;
|
||||||
|
|
||||||
|
@ -64,6 +61,19 @@ const executionDataFactory = (): ExecutionSummaryWithScopes => ({
|
||||||
scopes: ['workflow:update'],
|
scopes: ['workflow:update'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(WorkflowExecutionsPreview, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
// UN STUB router-link
|
||||||
|
'router-link': RouterLink,
|
||||||
|
},
|
||||||
|
plugins: [router],
|
||||||
|
mocks: {
|
||||||
|
$route,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('WorkflowExecutionsPreview.vue', () => {
|
describe('WorkflowExecutionsPreview.vue', () => {
|
||||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||||
|
@ -93,30 +103,26 @@ describe('WorkflowExecutionsPreview.vue', () => {
|
||||||
|
|
||||||
vi.spyOn(workflowsStore, 'getWorkflowById').mockReturnValue({ scopes } as IWorkflowDb);
|
vi.spyOn(workflowsStore, 'getWorkflowById').mockReturnValue({ scopes } as IWorkflowDb);
|
||||||
|
|
||||||
// Not using createComponentRenderer helper here because this component should not stub `router-link`
|
const { getByTestId } = renderComponent({ props: { execution: executionData } });
|
||||||
const { getByTestId } = render(WorkflowExecutionsPreview, {
|
|
||||||
props: {
|
|
||||||
execution: executionData,
|
|
||||||
},
|
|
||||||
global: {
|
|
||||||
plugins: [
|
|
||||||
I18nPlugin,
|
|
||||||
i18nInstance,
|
|
||||||
PiniaVuePlugin,
|
|
||||||
FontAwesomePlugin,
|
|
||||||
GlobalComponentsPlugin,
|
|
||||||
pinia,
|
|
||||||
router,
|
|
||||||
],
|
|
||||||
mocks: {
|
|
||||||
$route,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await userEvent.click(getByTestId('execution-debug-button'));
|
await userEvent.click(getByTestId('execution-debug-button'));
|
||||||
|
|
||||||
expect(router.currentRoute.value.path).toBe(path);
|
expect(router.currentRoute.value.path).toBe(path);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it('disables the stop execution button when the user cannot update', () => {
|
||||||
|
settingsStore.settings.enterprise = {
|
||||||
|
...(settingsStore.settings.enterprise ?? {}),
|
||||||
|
};
|
||||||
|
vi.spyOn(workflowsStore, 'getWorkflowById').mockReturnValue({
|
||||||
|
scopes: undefined,
|
||||||
|
} as IWorkflowDb);
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: { execution: { ...executionData, status: 'running' } },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('stop-execution')).toBeDisabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -120,7 +120,13 @@ function onRetryButtonBlur(event: FocusEvent) {
|
||||||
<N8nText :class="$style.runningMessage" color="text-light">
|
<N8nText :class="$style.runningMessage" color="text-light">
|
||||||
{{ locale.baseText('executionDetails.runningMessage') }}
|
{{ locale.baseText('executionDetails.runningMessage') }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<N8nButton class="mt-l" type="tertiary" @click="handleStopClick">
|
<N8nButton
|
||||||
|
data-test-id="stop-execution"
|
||||||
|
class="mt-l"
|
||||||
|
type="tertiary"
|
||||||
|
:disabled="!workflowPermissions.execute"
|
||||||
|
@click="handleStopClick"
|
||||||
|
>
|
||||||
{{ locale.baseText('executionsList.stopExecution') }}
|
{{ locale.baseText('executionsList.stopExecution') }}
|
||||||
</N8nButton>
|
</N8nButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -600,6 +600,7 @@
|
||||||
"credentials.item.updated": "Last updated",
|
"credentials.item.updated": "Last updated",
|
||||||
"credentials.item.created": "Created",
|
"credentials.item.created": "Created",
|
||||||
"credentials.item.owner": "Owner",
|
"credentials.item.owner": "Owner",
|
||||||
|
"credentials.item.readonly": "Read only",
|
||||||
"credentials.search.placeholder": "Search credentials...",
|
"credentials.search.placeholder": "Search credentials...",
|
||||||
"credentials.filters.type": "Type",
|
"credentials.filters.type": "Type",
|
||||||
"credentials.filters.active": "Some credentials may be hidden since filters are applied.",
|
"credentials.filters.active": "Some credentials may be hidden since filters are applied.",
|
||||||
|
@ -2206,6 +2207,7 @@
|
||||||
"workflows.item.move": "Move",
|
"workflows.item.move": "Move",
|
||||||
"workflows.item.updated": "Last updated",
|
"workflows.item.updated": "Last updated",
|
||||||
"workflows.item.created": "Created",
|
"workflows.item.created": "Created",
|
||||||
|
"workflows.item.readonly": "Read only",
|
||||||
"workflows.search.placeholder": "Search workflows...",
|
"workflows.search.placeholder": "Search workflows...",
|
||||||
"workflows.filters": "Filters",
|
"workflows.filters": "Filters",
|
||||||
"workflows.filters.tags": "Tags",
|
"workflows.filters.tags": "Tags",
|
||||||
|
|
|
@ -76,5 +76,40 @@ describe('CredentialsView', () => {
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should disable cards based on permissions', () => {
|
||||||
|
vi.spyOn(credentialsStore, 'allCredentials', 'get').mockReturnValue([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'test',
|
||||||
|
type: 'test',
|
||||||
|
createdAt: '2021-05-05T00:00:00Z',
|
||||||
|
updatedAt: '2021-05-05T00:00:00Z',
|
||||||
|
scopes: ['credential:update'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'test2',
|
||||||
|
type: 'test2',
|
||||||
|
createdAt: '2021-05-05T00:00:00Z',
|
||||||
|
updatedAt: '2021-05-05T00:00:00Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
expect(ResourcesListLayout.setup).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
resources: [
|
||||||
|
expect.objectContaining({
|
||||||
|
readOnly: false,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
readOnly: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -57,6 +57,7 @@ export default defineComponent({
|
||||||
scopes: credential.scopes,
|
scopes: credential.scopes,
|
||||||
type: credential.type,
|
type: credential.type,
|
||||||
sharedWithProjects: credential.sharedWithProjects,
|
sharedWithProjects: credential.sharedWithProjects,
|
||||||
|
readOnly: !getResourcePermissions(credential.scopes).credential.update,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
allCredentialTypes(): ICredentialType[] {
|
allCredentialTypes(): ICredentialType[] {
|
||||||
|
@ -179,7 +180,12 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<CredentialCard data-test-id="resources-list-item" class="mb-2xs" :data="data" />
|
<CredentialCard
|
||||||
|
data-test-id="resources-list-item"
|
||||||
|
class="mb-2xs"
|
||||||
|
:data="data"
|
||||||
|
:read-only="data.readOnly"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #filters="{ setKeyValue }">
|
<template #filters="{ setKeyValue }">
|
||||||
<div class="mb-s">
|
<div class="mb-s">
|
||||||
|
|
Loading…
Reference in a new issue