fix(editor): Follow up fixes and improvements to viewer role (#10684)

This commit is contained in:
Raúl Gómez Morales 2024-09-10 11:06:05 +02:00 committed by GitHub
parent efa5573278
commit 63548e6ead
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 109 additions and 34 deletions

View file

@ -85,4 +85,10 @@ describe('CredentialCard', () => {
}
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');
});
});

View file

@ -138,16 +138,19 @@ function moveResource() {
<template #header>
<n8n-heading tag="h2" bold :class="$style.cardHeading">
{{ data.name }}
<N8nBadge v-if="readOnly" class="ml-3xs" theme="tertiary" bold>
{{ locale.baseText('credentials.item.readonly') }}
</N8nBadge>
</n8n-heading>
</template>
<div :class="$style.cardDescription">
<n8n-text color="text-light" size="small">
<span v-if="credentialType">{{ credentialType.displayName }} | </span>
<span v-show="data"
>{{ $locale.baseText('credentials.item.updated') }} <TimeAgo :date="data.updatedAt" /> |
>{{ locale.baseText('credentials.item.updated') }} <TimeAgo :date="data.updatedAt" /> |
</span>
<span v-show="data"
>{{ $locale.baseText('credentials.item.created') }} {{ formattedCreatedAtDate }}
>{{ locale.baseText('credentials.item.created') }} {{ formattedCreatedAtDate }}
</span>
</n8n-text>
</div>

View file

@ -407,7 +407,7 @@ async function beforeClose() {
},
);
keepEditing = confirmAction === MODAL_CONFIRM;
} else if (isOAuthType.value && !isOAuthConnected.value) {
} else if (credentialPermissions.value.update && isOAuthType.value && !isOAuthConnected.value) {
const confirmAction = await message.confirm(
i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.message'),
i18n.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.headline'),

View file

@ -57,7 +57,7 @@ describe('WorkflowCard', () => {
it('should render a card with the workflow name and open workflow clicking on it', async () => {
const data = createWorkflow();
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();
@ -166,4 +166,12 @@ describe('WorkflowCard', () => {
}
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');
});
});

View file

@ -236,16 +236,19 @@ function moveResource() {
<template #header>
<n8n-heading tag="h2" bold :class="$style.cardHeading" data-test-id="workflow-card-name">
{{ data.name }}
<N8nBadge v-if="!workflowPermissions.update" class="ml-3xs" theme="tertiary" bold>
{{ locale.baseText('workflows.item.readonly') }}
</N8nBadge>
</n8n-heading>
</template>
<div :class="$style.cardDescription">
<n8n-text color="text-light" size="small">
<span v-show="data"
>{{ $locale.baseText('workflows.item.updated') }}
>{{ locale.baseText('workflows.item.updated') }}
<TimeAgo :date="String(data.updatedAt)" /> |
</span>
<span v-show="data" class="mr-2xs"
>{{ $locale.baseText('workflows.item.created') }} {{ formattedCreatedAtDate }}
>{{ locale.baseText('workflows.item.created') }} {{ formattedCreatedAtDate }}
</span>
<span
v-if="settingsStore.areTagsEnabled && data.tags && data.tags.length > 0"

View file

@ -1,18 +1,15 @@
import { describe, expect } from 'vitest';
import { render } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { faker } from '@faker-js/faker';
import { createRouter, createWebHistory } from 'vue-router';
import { createPinia, PiniaVuePlugin, setActivePinia } from 'pinia';
import { createRouter, createWebHistory, RouterLink } from 'vue-router';
import { createPinia, setActivePinia } from 'pinia';
import { randomInt, type ExecutionSummary } from 'n8n-workflow';
import { useSettingsStore } from '@/stores/settings.store';
import WorkflowExecutionsPreview from '@/components/executions/workflow/WorkflowExecutionsPreview.vue';
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 type { ExecutionSummaryWithScopes, IWorkflowDb } from '@/Interface';
import { createComponentRenderer } from '@/__tests__/render';
let pinia: ReturnType<typeof createPinia>;
@ -64,6 +61,19 @@ const executionDataFactory = (): ExecutionSummaryWithScopes => ({
scopes: ['workflow:update'],
});
const renderComponent = createComponentRenderer(WorkflowExecutionsPreview, {
global: {
stubs: {
// UN STUB router-link
'router-link': RouterLink,
},
plugins: [router],
mocks: {
$route,
},
},
});
describe('WorkflowExecutionsPreview.vue', () => {
let settingsStore: ReturnType<typeof useSettingsStore>;
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
@ -93,30 +103,26 @@ describe('WorkflowExecutionsPreview.vue', () => {
vi.spyOn(workflowsStore, 'getWorkflowById').mockReturnValue({ scopes } as IWorkflowDb);
// Not using createComponentRenderer helper here because this component should not stub `router-link`
const { getByTestId } = render(WorkflowExecutionsPreview, {
props: {
execution: executionData,
},
global: {
plugins: [
I18nPlugin,
i18nInstance,
PiniaVuePlugin,
FontAwesomePlugin,
GlobalComponentsPlugin,
pinia,
router,
],
mocks: {
$route,
},
},
});
const { getByTestId } = renderComponent({ props: { execution: executionData } });
await userEvent.click(getByTestId('execution-debug-button'));
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();
});
});

View file

@ -120,7 +120,13 @@ function onRetryButtonBlur(event: FocusEvent) {
<N8nText :class="$style.runningMessage" color="text-light">
{{ locale.baseText('executionDetails.runningMessage') }}
</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') }}
</N8nButton>
</div>

View file

@ -600,6 +600,7 @@
"credentials.item.updated": "Last updated",
"credentials.item.created": "Created",
"credentials.item.owner": "Owner",
"credentials.item.readonly": "Read only",
"credentials.search.placeholder": "Search credentials...",
"credentials.filters.type": "Type",
"credentials.filters.active": "Some credentials may be hidden since filters are applied.",
@ -2206,6 +2207,7 @@
"workflows.item.move": "Move",
"workflows.item.updated": "Last updated",
"workflows.item.created": "Created",
"workflows.item.readonly": "Read only",
"workflows.search.placeholder": "Search workflows...",
"workflows.filters": "Filters",
"workflows.filters.tags": "Tags",

View file

@ -76,5 +76,40 @@ describe('CredentialsView', () => {
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,
);
});
});
});

View file

@ -57,6 +57,7 @@ export default defineComponent({
scopes: credential.scopes,
type: credential.type,
sharedWithProjects: credential.sharedWithProjects,
readOnly: !getResourcePermissions(credential.scopes).credential.update,
}));
},
allCredentialTypes(): ICredentialType[] {
@ -179,7 +180,12 @@ export default defineComponent({
</div>
</template>
<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 #filters="{ setKeyValue }">
<div class="mb-s">