mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Optimizing main sidebar to have more space for Projects (#9686)
Co-authored-by: Omar Ajoue <krynble@gmail.com>
This commit is contained in:
parent
31c456700a
commit
5cdcb61f66
|
@ -47,28 +47,28 @@ describe('Undo/Redo', () => {
|
||||||
SET_NODE_NAME,
|
SET_NODE_NAME,
|
||||||
);
|
);
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters.canvasNodeByName('Code').then(($codeNode) => {
|
||||||
.canvasNodeByName('Code')
|
const cssLeft = parseInt($codeNode.css('left'));
|
||||||
.should('have.css', 'left', '860px')
|
const cssTop = parseInt($codeNode.css('top'));
|
||||||
.should('have.css', 'top', '220px');
|
|
||||||
|
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.actions.hitUndo();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.actions.hitUndo();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
|
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
||||||
WorkflowPage.actions.hitRedo();
|
WorkflowPage.actions.hitRedo();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
WorkflowPage.actions.hitRedo();
|
WorkflowPage.actions.hitRedo();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 3);
|
WorkflowPage.getters.canvasNodes().should('have.have.length', 3);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 2);
|
WorkflowPage.getters.nodeConnections().should('have.length', 2);
|
||||||
// Last node should be added back to original position
|
// Last node should be added back to original position
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodeByName('Code')
|
.canvasNodeByName('Code')
|
||||||
.should('have.css', 'left', '860px')
|
.should('have.css', 'left', cssLeft + 'px')
|
||||||
.should('have.css', 'top', '220px');
|
.should('have.css', 'top', cssTop + 'px');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should undo/redo deleting node using context menu', () => {
|
it('should undo/redo deleting node using context menu', () => {
|
||||||
|
@ -135,22 +135,30 @@ describe('Undo/Redo', () => {
|
||||||
it('should undo/redo moving nodes', () => {
|
it('should undo/redo moving nodes', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => {
|
||||||
WorkflowPage.getters
|
const initialPosition = $node.position();
|
||||||
.canvasNodeByName('Code')
|
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
||||||
.should('have.css', 'left', '740px')
|
|
||||||
.should('have.css', 'top', '320px');
|
|
||||||
|
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => {
|
||||||
WorkflowPage.getters
|
const cssLeft = parseInt($node.css('left'));
|
||||||
.canvasNodeByName('Code')
|
const cssTop = parseInt($node.css('top'));
|
||||||
.should('have.css', 'left', '640px')
|
expect(cssLeft).to.be.greaterThan(initialPosition.left);
|
||||||
.should('have.css', 'top', '220px');
|
expect(cssTop).to.be.greaterThan(initialPosition.top);
|
||||||
WorkflowPage.actions.hitRedo();
|
});
|
||||||
WorkflowPage.getters
|
|
||||||
.canvasNodeByName('Code')
|
WorkflowPage.actions.hitUndo();
|
||||||
.should('have.css', 'left', '740px')
|
WorkflowPage.getters
|
||||||
.should('have.css', 'top', '320px');
|
.canvasNodeByName(CODE_NODE_NAME)
|
||||||
|
.should('have.css', 'left', `${initialPosition.left}px`)
|
||||||
|
.should('have.css', 'top', `${initialPosition.top}px`);
|
||||||
|
WorkflowPage.actions.hitRedo();
|
||||||
|
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).then(($node) => {
|
||||||
|
const cssLeft = parseInt($node.css('left'));
|
||||||
|
const cssTop = parseInt($node.css('top'));
|
||||||
|
expect(cssLeft).to.be.greaterThan(initialPosition.left);
|
||||||
|
expect(cssTop).to.be.greaterThan(initialPosition.top);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should undo/redo deleting a connection using context menu', () => {
|
it('should undo/redo deleting a connection using context menu', () => {
|
||||||
|
@ -292,8 +300,12 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodes()
|
.canvasNodes()
|
||||||
.first()
|
.first()
|
||||||
.should('have.css', 'left', `${initialPosition.left + 120}px`)
|
.then(($node) => {
|
||||||
.should('have.css', 'top', `${initialPosition.top + 140}px`);
|
const cssLeft = parseInt($node.css('left'));
|
||||||
|
const cssTop = parseInt($node.css('top'));
|
||||||
|
expect(cssLeft).to.be.greaterThan(initialPosition.left);
|
||||||
|
expect(cssTop).to.be.greaterThan(initialPosition.top);
|
||||||
|
});
|
||||||
|
|
||||||
// Delete the set node
|
// Delete the set node
|
||||||
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click();
|
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click();
|
||||||
|
@ -322,8 +334,12 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodes()
|
.canvasNodes()
|
||||||
.first()
|
.first()
|
||||||
.should('have.css', 'left', `${initialPosition.left + 120}px`)
|
.then(($node) => {
|
||||||
.should('have.css', 'top', `${initialPosition.top + 140}px`);
|
const cssLeft = parseInt($node.css('left'));
|
||||||
|
const cssTop = parseInt($node.css('top'));
|
||||||
|
expect(cssLeft).to.be.greaterThan(initialPosition.left);
|
||||||
|
expect(cssTop).to.be.greaterThan(initialPosition.top);
|
||||||
|
});
|
||||||
// Third redo: Should delete the Set node
|
// Third redo: Should delete the Set node
|
||||||
WorkflowPage.actions.hitRedo();
|
WorkflowPage.actions.hitRedo();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||||
|
@ -340,9 +356,6 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
|
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
|
||||||
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch'))
|
|
||||||
.should('have.css', 'left', '637px')
|
|
||||||
.should('have.css', 'top', '501px');
|
|
||||||
|
|
||||||
cy.fixture('Test_workflow_form_switch.json').then((data) => {
|
cy.fixture('Test_workflow_form_switch.json').then((data) => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
|
@ -355,9 +368,6 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
|
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
|
||||||
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch'))
|
|
||||||
.should('have.css', 'left', '637px')
|
|
||||||
.should('have.css', 'top', '501px');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not undo/redo when NDV or a modal is open', () => {
|
it('should not undo/redo when NDV or a modal is open', () => {
|
||||||
|
|
|
@ -14,6 +14,7 @@ describe('Inline expression editor', () => {
|
||||||
describe('Static data', () => {
|
describe('Static data', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Hacker News');
|
||||||
WorkflowPage.actions.openInlineExpressionEditor();
|
WorkflowPage.actions.openInlineExpressionEditor();
|
||||||
});
|
});
|
||||||
|
@ -75,6 +76,7 @@ describe('Inline expression editor', () => {
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Hacker News');
|
||||||
WorkflowPage.actions.openInlineExpressionEditor();
|
WorkflowPage.actions.openInlineExpressionEditor();
|
||||||
});
|
});
|
||||||
|
|
|
@ -125,6 +125,8 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
|
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||||
|
WorkflowPage.getters.nodeConnections().should('have.length', 2);
|
||||||
WorkflowPage.actions.addNodeBetweenNodes(
|
WorkflowPage.actions.addNodeBetweenNodes(
|
||||||
CODE_NODE_NAME,
|
CODE_NODE_NAME,
|
||||||
EDIT_FIELDS_SET_NODE_NAME,
|
EDIT_FIELDS_SET_NODE_NAME,
|
||||||
|
@ -132,12 +134,15 @@ describe('Canvas Actions', () => {
|
||||||
);
|
);
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
||||||
// And last node should be pushed to the right
|
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
|
||||||
.canvasNodes()
|
const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left'));
|
||||||
.last()
|
|
||||||
.should('have.css', 'left', '860px')
|
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
|
||||||
.should('have.css', 'top', '220px');
|
const httpNodeLeft = parseFloat($httpNode.css('left'));
|
||||||
|
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete connections by pressing the delete button', () => {
|
it('should delete connections by pressing the delete button', () => {
|
||||||
|
|
|
@ -69,7 +69,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
|
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
|
||||||
WorkflowPage.getters.nodeViewBackground().click(600 + i * 100, 200, { force: true });
|
WorkflowPage.getters
|
||||||
|
.nodeViewBackground()
|
||||||
|
.click((i + 1) * 200, (i + 1) * 200, { force: true });
|
||||||
}
|
}
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
|
@ -197,13 +199,23 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodes()
|
.canvasNodes()
|
||||||
.last()
|
.last()
|
||||||
.should('have.css', 'left', '740px')
|
.then(($node) => {
|
||||||
.should('have.css', 'top', '320px');
|
const { left, top } = $node.position();
|
||||||
|
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
|
||||||
|
clickToFinish: true,
|
||||||
|
});
|
||||||
|
WorkflowPage.getters
|
||||||
|
.canvasNodes()
|
||||||
|
.last()
|
||||||
|
.then(($node) => {
|
||||||
|
const { left: newLeft, top: newTop } = $node.position();
|
||||||
|
expect(newLeft).to.be.greaterThan(left);
|
||||||
|
expect(newTop).to.be.greaterThan(top);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should zoom in', () => {
|
it('should zoom in', () => {
|
||||||
|
|
|
@ -109,6 +109,8 @@ describe('Data pinning', () => {
|
||||||
.parent()
|
.parent()
|
||||||
.should('have.class', 'is-disabled');
|
.should('have.class', 'is-disabled');
|
||||||
|
|
||||||
|
cy.get('body').type('{esc}');
|
||||||
|
|
||||||
// Unpin using context menu
|
// Unpin using context menu
|
||||||
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
ndv.actions.setPinnedData([{ test: 1 }]);
|
ndv.actions.setPinnedData([{ test: 1 }]);
|
||||||
|
|
|
@ -18,6 +18,8 @@ describe('Data mapping', () => {
|
||||||
cy.fixture('Test_workflow-actions_paste-data.json').then((data) => {
|
cy.fixture('Test_workflow-actions_paste-data.json').then((data) => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
workflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.executePrevious();
|
ndv.actions.executePrevious();
|
||||||
ndv.actions.switchInputMode('Table');
|
ndv.actions.switchInputMode('Table');
|
||||||
|
@ -49,6 +51,7 @@ describe('Data mapping', () => {
|
||||||
cy.fixture('Test_workflow_3.json').then((data) => {
|
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
workflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.switchInputMode('Table');
|
ndv.actions.switchInputMode('Table');
|
||||||
|
@ -111,6 +114,7 @@ describe('Data mapping', () => {
|
||||||
cy.fixture('Test_workflow_3.json').then((data) => {
|
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
workflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.switchInputMode('JSON');
|
ndv.actions.switchInputMode('JSON');
|
||||||
|
@ -149,6 +153,7 @@ describe('Data mapping', () => {
|
||||||
cy.fixture('Test_workflow_3.json').then((data) => {
|
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
workflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.clearParameterInput('value');
|
ndv.actions.clearParameterInput('value');
|
||||||
|
@ -255,6 +260,7 @@ describe('Data mapping', () => {
|
||||||
cy.fixture('Test_workflow_3.json').then((data) => {
|
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
workflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
|
|
||||||
|
@ -286,6 +292,7 @@ describe('Data mapping', () => {
|
||||||
cy.fixture('Test_workflow_3.json').then((data) => {
|
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
workflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.typeIntoParameterInput('value', 'test_value');
|
ndv.actions.typeIntoParameterInput('value', 'test_value');
|
||||||
|
@ -307,6 +314,7 @@ describe('Data mapping', () => {
|
||||||
cy.fixture('Test_workflow_3.json').then((data) => {
|
cy.fixture('Test_workflow_3.json').then((data) => {
|
||||||
cy.get('body').paste(JSON.stringify(data));
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
workflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
workflowPage.actions.openNode('Set');
|
workflowPage.actions.openNode('Set');
|
||||||
ndv.actions.clearParameterInput('value');
|
ndv.actions.clearParameterInput('value');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
|
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||||
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||||
import { errorToast, successToast } from '../pages/notifications';
|
import { clearNotifications, errorToast, successToast } from '../pages/notifications';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPageClass();
|
const workflowPage = new WorkflowPageClass();
|
||||||
const executionsTab = new WorkflowExecutionsTab();
|
const executionsTab = new WorkflowExecutionsTab();
|
||||||
|
@ -62,13 +62,13 @@ describe('Execution', () => {
|
||||||
.within(() => cy.get('.fa-check'))
|
.within(() => cy.get('.fa-check'))
|
||||||
.should('exist');
|
.should('exist');
|
||||||
|
|
||||||
|
successToast().should('be.visible');
|
||||||
|
clearNotifications();
|
||||||
|
|
||||||
// Clear execution data
|
// Clear execution data
|
||||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||||
workflowPage.getters.clearExecutionDataButton().click();
|
workflowPage.getters.clearExecutionDataButton().click();
|
||||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||||
|
|
||||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
|
||||||
successToast().should('be.visible');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should test manual workflow stop', () => {
|
it('should test manual workflow stop', () => {
|
||||||
|
@ -106,6 +106,9 @@ describe('Execution', () => {
|
||||||
.canvasNodeByName('Set')
|
.canvasNodeByName('Set')
|
||||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
|
||||||
|
successToast().should('be.visible');
|
||||||
|
clearNotifications();
|
||||||
|
|
||||||
workflowPage.getters.stopExecutionButton().should('exist');
|
workflowPage.getters.stopExecutionButton().should('exist');
|
||||||
workflowPage.getters.stopExecutionButton().click();
|
workflowPage.getters.stopExecutionButton().click();
|
||||||
|
|
||||||
|
@ -121,13 +124,13 @@ describe('Execution', () => {
|
||||||
.canvasNodeByName('Set')
|
.canvasNodeByName('Set')
|
||||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
|
||||||
|
successToast().should('be.visible');
|
||||||
|
clearNotifications();
|
||||||
|
|
||||||
// Clear execution data
|
// Clear execution data
|
||||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||||
workflowPage.getters.clearExecutionDataButton().click();
|
workflowPage.getters.clearExecutionDataButton().click();
|
||||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||||
|
|
||||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
|
||||||
successToast().should('be.visible');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should test webhook workflow', () => {
|
it('should test webhook workflow', () => {
|
||||||
|
@ -194,13 +197,13 @@ describe('Execution', () => {
|
||||||
.within(() => cy.get('.fa-check'))
|
.within(() => cy.get('.fa-check'))
|
||||||
.should('exist');
|
.should('exist');
|
||||||
|
|
||||||
|
successToast().should('be.visible');
|
||||||
|
clearNotifications();
|
||||||
|
|
||||||
// Clear execution data
|
// Clear execution data
|
||||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||||
workflowPage.getters.clearExecutionDataButton().click();
|
workflowPage.getters.clearExecutionDataButton().click();
|
||||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||||
|
|
||||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
|
||||||
successToast().should('be.visible');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should test webhook workflow stop', () => {
|
it('should test webhook workflow stop', () => {
|
||||||
|
@ -239,6 +242,9 @@ describe('Execution', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
successToast().should('be.visible');
|
||||||
|
clearNotifications();
|
||||||
|
|
||||||
workflowPage.getters.stopExecutionButton().click();
|
workflowPage.getters.stopExecutionButton().click();
|
||||||
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
// Check canvas nodes after 1st step (workflow passed the manual trigger node
|
||||||
workflowPage.getters
|
workflowPage.getters
|
||||||
|
@ -268,13 +274,13 @@ describe('Execution', () => {
|
||||||
.canvasNodeByName('Set')
|
.canvasNodeByName('Set')
|
||||||
.within(() => cy.get('.fa-check').should('not.exist'));
|
.within(() => cy.get('.fa-check').should('not.exist'));
|
||||||
|
|
||||||
|
successToast().should('be.visible');
|
||||||
|
clearNotifications();
|
||||||
|
|
||||||
// Clear execution data
|
// Clear execution data
|
||||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||||
workflowPage.getters.clearExecutionDataButton().click();
|
workflowPage.getters.clearExecutionDataButton().click();
|
||||||
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
workflowPage.getters.clearExecutionDataButton().should('not.exist');
|
||||||
|
|
||||||
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
|
|
||||||
successToast().should('be.visible');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('execution preview', () => {
|
describe('execution preview', () => {
|
||||||
|
|
|
@ -26,6 +26,9 @@ function checkStickiesStyle(
|
||||||
describe('Canvas Actions', () => {
|
describe('Canvas Actions', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
workflowPage.actions.visit();
|
workflowPage.actions.visit();
|
||||||
|
cy.get('#collapse-change-button').should('be.visible').click();
|
||||||
|
cy.get('#side-menu[class*=collapsed i]').should('be.visible');
|
||||||
|
workflowPage.actions.zoomToFit();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds sticky to canvas with default text and position', () => {
|
it('adds sticky to canvas with default text and position', () => {
|
||||||
|
|
|
@ -14,6 +14,7 @@ describe('Expression editor modal', () => {
|
||||||
describe('Static data', () => {
|
describe('Static data', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Hacker News');
|
||||||
WorkflowPage.actions.openExpressionEditorModal();
|
WorkflowPage.actions.openExpressionEditorModal();
|
||||||
});
|
});
|
||||||
|
@ -69,6 +70,7 @@ describe('Expression editor modal', () => {
|
||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
||||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||||
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.actions.openNode('Hacker News');
|
WorkflowPage.actions.openNode('Hacker News');
|
||||||
WorkflowPage.actions.openExpressionEditorModal();
|
WorkflowPage.actions.openExpressionEditorModal();
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,16 +11,14 @@ export class MainSidebar extends BasePage {
|
||||||
credentials: () => this.getters.menuItem('credentials'),
|
credentials: () => this.getters.menuItem('credentials'),
|
||||||
executions: () => this.getters.menuItem('executions'),
|
executions: () => this.getters.menuItem('executions'),
|
||||||
adminPanel: () => this.getters.menuItem('cloud-admin'),
|
adminPanel: () => this.getters.menuItem('cloud-admin'),
|
||||||
userMenu: () => cy.get('div[class="action-dropdown-container"]'),
|
userMenu: () => cy.getByTestId('user-menu'),
|
||||||
logo: () => cy.getByTestId('n8n-logo'),
|
logo: () => cy.getByTestId('n8n-logo'),
|
||||||
};
|
};
|
||||||
|
|
||||||
actions = {
|
actions = {
|
||||||
goToSettings: () => {
|
goToSettings: () => {
|
||||||
this.getters.settings().should('be.visible');
|
this.getters.userMenu().click();
|
||||||
// We must wait before ElementUI menu is done with its animations
|
cy.getByTestId('user-menu-item-settings').should('be.visible').click();
|
||||||
cy.get('[data-old-overflow]').should('not.exist');
|
|
||||||
this.getters.settings().click();
|
|
||||||
},
|
},
|
||||||
goToCredentials: () => {
|
goToCredentials: () => {
|
||||||
this.getters.credentials().should('be.visible');
|
this.getters.credentials().should('be.visible');
|
||||||
|
|
|
@ -28,10 +28,7 @@
|
||||||
|
|
||||||
<template #beforeLowerMenu>
|
<template #beforeLowerMenu>
|
||||||
<BecomeTemplateCreatorCta v-if="fullyExpanded && !userIsTrialing" />
|
<BecomeTemplateCreatorCta v-if="fullyExpanded && !userIsTrialing" />
|
||||||
<ExecutionsUsage
|
</template>
|
||||||
v-if="fullyExpanded && userIsTrialing"
|
|
||||||
:cloud-plan-data="currentPlanAndUsageData"
|
|
||||||
/></template>
|
|
||||||
<template #menuSuffix>
|
<template #menuSuffix>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
@ -106,7 +103,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { CloudPlanAndUsageData, IExecutionResponse, IMenuItem, IVersion } from '@/Interface';
|
import type { IExecutionResponse, IMenuItem, IVersion } from '@/Interface';
|
||||||
import GiftNotificationIcon from './GiftNotificationIcon.vue';
|
import GiftNotificationIcon from './GiftNotificationIcon.vue';
|
||||||
|
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
|
@ -123,7 +120,6 @@ import { useUsersStore } from '@/stores/users.store';
|
||||||
import { useVersionsStore } from '@/stores/versions.store';
|
import { useVersionsStore } from '@/stores/versions.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import ExecutionsUsage from '@/components/executions/ExecutionsUsage.vue';
|
|
||||||
import BecomeTemplateCreatorCta from '@/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue';
|
import BecomeTemplateCreatorCta from '@/components/BecomeTemplateCreatorCta/BecomeTemplateCreatorCta.vue';
|
||||||
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
|
import MainSidebarSourceControl from '@/components/MainSidebarSourceControl.vue';
|
||||||
import { hasPermission } from '@/utils/rbac/permissions';
|
import { hasPermission } from '@/utils/rbac/permissions';
|
||||||
|
@ -137,7 +133,6 @@ export default defineComponent({
|
||||||
name: 'MainSidebar',
|
name: 'MainSidebar',
|
||||||
components: {
|
components: {
|
||||||
GiftNotificationIcon,
|
GiftNotificationIcon,
|
||||||
ExecutionsUsage,
|
|
||||||
MainSidebarSourceControl,
|
MainSidebarSourceControl,
|
||||||
BecomeTemplateCreatorCta,
|
BecomeTemplateCreatorCta,
|
||||||
ProjectNavigation,
|
ProjectNavigation,
|
||||||
|
@ -189,10 +184,6 @@ export default defineComponent({
|
||||||
isCollapsed(): boolean {
|
isCollapsed(): boolean {
|
||||||
return this.uiStore.sidebarMenuCollapsed;
|
return this.uiStore.sidebarMenuCollapsed;
|
||||||
},
|
},
|
||||||
canUserAccessSettings(): boolean {
|
|
||||||
const accessibleRoute = this.findFirstAccessibleSettingsRoute();
|
|
||||||
return accessibleRoute !== null;
|
|
||||||
},
|
|
||||||
showUserArea(): boolean {
|
showUserArea(): boolean {
|
||||||
return hasPermission(['authenticated']);
|
return hasPermission(['authenticated']);
|
||||||
},
|
},
|
||||||
|
@ -212,10 +203,7 @@ export default defineComponent({
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
mainMenuItems(): IMenuItem[] {
|
mainMenuItems(): IMenuItem[] {
|
||||||
const items: IMenuItem[] = [];
|
const items: IMenuItem[] = [
|
||||||
|
|
||||||
const defaultSettingsRoute = this.findFirstAccessibleSettingsRoute();
|
|
||||||
const regularItems: IMenuItem[] = [
|
|
||||||
{
|
{
|
||||||
id: 'cloud-admin',
|
id: 'cloud-admin',
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
|
@ -261,15 +249,6 @@ export default defineComponent({
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
route: { to: { name: VIEWS.EXECUTIONS } },
|
route: { to: { name: VIEWS.EXECUTIONS } },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'settings',
|
|
||||||
icon: 'cog',
|
|
||||||
label: this.$locale.baseText('settings'),
|
|
||||||
position: 'bottom',
|
|
||||||
available: this.canUserAccessSettings && this.usersStore.currentUser !== null,
|
|
||||||
activateOnRouteNames: [VIEWS.USERS_SETTINGS, VIEWS.API_SETTINGS, VIEWS.PERSONAL_SETTINGS],
|
|
||||||
route: { to: defaultSettingsRoute },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'help',
|
id: 'help',
|
||||||
icon: 'question',
|
icon: 'question',
|
||||||
|
@ -321,20 +300,11 @@ export default defineComponent({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
return [...items, ...regularItems];
|
return items;
|
||||||
},
|
},
|
||||||
userIsTrialing(): boolean {
|
userIsTrialing(): boolean {
|
||||||
return this.cloudPlanStore.userIsTrialing;
|
return this.cloudPlanStore.userIsTrialing;
|
||||||
},
|
},
|
||||||
currentPlanAndUsageData(): CloudPlanAndUsageData | null {
|
|
||||||
const planData = this.cloudPlanStore.currentPlanData;
|
|
||||||
const usage = this.cloudPlanStore.currentUsageData;
|
|
||||||
if (!planData || !usage) return null;
|
|
||||||
return {
|
|
||||||
...planData,
|
|
||||||
usage,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.basePath = this.rootStore.baseUrl;
|
this.basePath = this.rootStore.baseUrl;
|
||||||
|
@ -345,12 +315,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
void this.$nextTick(() => {
|
void this.$nextTick(() => {
|
||||||
if (window.innerWidth < 900 || this.uiStore.isNodeView) {
|
this.uiStore.sidebarMenuCollapsed = window.innerWidth < 900;
|
||||||
this.uiStore.sidebarMenuCollapsed = true;
|
|
||||||
} else {
|
|
||||||
this.uiStore.sidebarMenuCollapsed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fullyExpanded = !this.isCollapsed;
|
this.fullyExpanded = !this.isCollapsed;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -435,24 +400,6 @@ export default defineComponent({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
findFirstAccessibleSettingsRoute() {
|
|
||||||
const settingsRoutes = this.$router
|
|
||||||
.getRoutes()
|
|
||||||
.find((route) => route.path === '/settings')
|
|
||||||
?.children.map((route) => route.name ?? '');
|
|
||||||
|
|
||||||
let defaultSettingsRoute = { name: VIEWS.USERS_SETTINGS };
|
|
||||||
for (const route of settingsRoutes ?? []) {
|
|
||||||
if (this.canUserAccessRouteByName(route.toString())) {
|
|
||||||
defaultSettingsRoute = {
|
|
||||||
name: route.toString() as VIEWS,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultSettingsRoute;
|
|
||||||
},
|
|
||||||
onResize(event: UIEvent) {
|
onResize(event: UIEvent) {
|
||||||
void this.callDebounced(this.onResizeEnd, { debounceTime: 100 }, event);
|
void this.callDebounced(this.onResizeEnd, { debounceTime: 100 }, event);
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,18 +4,49 @@ import { i18n as locale } from '@/plugins/i18n';
|
||||||
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import type { CloudPlanAndUsageData } from '@/Interface';
|
||||||
|
|
||||||
const trialDaysLeft = computed(() => {
|
const PROGRESS_BAR_MINIMUM_THRESHOLD = 8;
|
||||||
const { trialDaysLeft } = useCloudPlanStore();
|
|
||||||
return -1 * trialDaysLeft;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const cloudPlanStore = useCloudPlanStore();
|
||||||
|
|
||||||
|
const trialDaysLeft = computed(() => -1 * cloudPlanStore.trialDaysLeft);
|
||||||
const messageText = computed(() => {
|
const messageText = computed(() => {
|
||||||
return locale.baseText('banners.trial.message', {
|
return locale.baseText('banners.trial.message', {
|
||||||
adjustToNumber: trialDaysLeft.value,
|
adjustToNumber: trialDaysLeft.value,
|
||||||
interpolate: { count: String(trialDaysLeft.value) },
|
interpolate: { count: String(trialDaysLeft.value) },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
const cloudPlanData = computed<CloudPlanAndUsageData | null>(() => {
|
||||||
|
const planData = cloudPlanStore.currentPlanData;
|
||||||
|
const usage = cloudPlanStore.currentUsageData;
|
||||||
|
if (!planData || !usage) return null;
|
||||||
|
return {
|
||||||
|
...planData,
|
||||||
|
usage,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const trialHasExecutionsLeft = computed(() => {
|
||||||
|
if (!cloudPlanData.value?.usage) return 0;
|
||||||
|
return cloudPlanData.value.usage.executions < cloudPlanData.value.monthlyExecutionsLimit;
|
||||||
|
});
|
||||||
|
const currentExecutionsWithThreshold = computed(() => {
|
||||||
|
if (!cloudPlanData.value?.usage) return 0;
|
||||||
|
const usedExecutions = cloudPlanData.value.usage.executions;
|
||||||
|
const executionsQuota = cloudPlanData.value.monthlyExecutionsLimit;
|
||||||
|
const threshold = (PROGRESS_BAR_MINIMUM_THRESHOLD * executionsQuota) / 100;
|
||||||
|
return usedExecutions < threshold ? threshold : usedExecutions;
|
||||||
|
});
|
||||||
|
const maxExecutions = computed(() => {
|
||||||
|
if (!cloudPlanData.value?.monthlyExecutionsLimit) return 0;
|
||||||
|
return cloudPlanData.value.monthlyExecutionsLimit;
|
||||||
|
});
|
||||||
|
const currentExecutions = computed(() => {
|
||||||
|
if (!cloudPlanData.value?.usage) return 0;
|
||||||
|
const usedExecutions = cloudPlanData.value.usage.executions;
|
||||||
|
const executionsQuota = cloudPlanData.value.monthlyExecutionsLimit;
|
||||||
|
return usedExecutions > executionsQuota ? executionsQuota : usedExecutions;
|
||||||
|
});
|
||||||
|
|
||||||
function onUpdatePlanClick() {
|
function onUpdatePlanClick() {
|
||||||
void useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
|
void useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
|
||||||
|
@ -25,7 +56,31 @@ function onUpdatePlanClick() {
|
||||||
<template>
|
<template>
|
||||||
<BaseBanner name="TRIAL" theme="custom">
|
<BaseBanner name="TRIAL" theme="custom">
|
||||||
<template #mainContent>
|
<template #mainContent>
|
||||||
<span>{{ messageText }}</span>
|
<div :class="$style.content">
|
||||||
|
<span>{{ messageText }}</span>
|
||||||
|
<span :class="$style.pipe"> | </span>
|
||||||
|
<div :class="$style.usageCounter">
|
||||||
|
<div :class="$style.progressBarDiv">
|
||||||
|
<progress
|
||||||
|
:class="[
|
||||||
|
trialHasExecutionsLeft ? $style.progressBarSuccess : $style.progressBarDanger,
|
||||||
|
$style.progressBar,
|
||||||
|
]"
|
||||||
|
:value="currentExecutionsWithThreshold"
|
||||||
|
:max="maxExecutions"
|
||||||
|
></progress>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.executionsCountSection">
|
||||||
|
<N8nText size="xsmall" :color="trialHasExecutionsLeft ? 'text-dark' : 'danger'">
|
||||||
|
{{ currentExecutions }}/{{ maxExecutions }} </N8nText
|
||||||
|
> <N8nText
|
||||||
|
size="xsmall"
|
||||||
|
:color="trialHasExecutionsLeft ? 'text-dark' : 'danger'"
|
||||||
|
>{{ locale.baseText('executionUsage.label.executions') }}</N8nText
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #trailingContent>
|
<template #trailingContent>
|
||||||
<n8n-button type="success" icon="gem" size="small" @click="onUpdatePlanClick">{{
|
<n8n-button type="success" icon="gem" size="small" @click="onUpdatePlanClick">{{
|
||||||
|
@ -34,3 +89,86 @@ function onUpdatePlanClick() {
|
||||||
</template>
|
</template>
|
||||||
</BaseBanner>
|
</BaseBanner>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-3xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipe {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
padding: 0 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBarDiv {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar {
|
||||||
|
width: 62.4px;
|
||||||
|
border: 0;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: var(--color-foreground-base);
|
||||||
|
}
|
||||||
|
.progressBar::-webkit-progress-bar {
|
||||||
|
width: 62.4px;
|
||||||
|
border: 0;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: var(--color-foreground-base);
|
||||||
|
}
|
||||||
|
.progressBar::-moz-progress-bar {
|
||||||
|
width: 62.4px;
|
||||||
|
border: 0;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: var(--color-foreground-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBarSuccess::-moz-progress-bar {
|
||||||
|
background: var(--color-foreground-xdark);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBarSuccess::-webkit-progress-value {
|
||||||
|
background: var(--color-foreground-xdark);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBarDanger::-webkit-progress-value {
|
||||||
|
background: var(--color-danger);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBarDanger::-moz-progress-bar {
|
||||||
|
background: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usageText {
|
||||||
|
margin-left: var(--spacing-s);
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
line-height: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.usageCounter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: var(--font-size-3xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.executionsCountSection {
|
||||||
|
margin-left: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,203 +0,0 @@
|
||||||
<template>
|
|
||||||
<div :class="$style.container">
|
|
||||||
<div v-if="isTrialExpired" :class="$style.usageText">
|
|
||||||
<n8n-text size="small" color="danger">
|
|
||||||
{{ locale.baseText('executionUsage.expired.text') }}
|
|
||||||
</n8n-text>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="!isTrialExpired && trialHasExecutionsLeft" :class="$style.usageText">
|
|
||||||
<i18n-t tag="span" keypath="executionUsage.currentUsage">
|
|
||||||
<template #text>
|
|
||||||
<n8n-text size="small" color="text-dark">
|
|
||||||
{{ locale.baseText('executionUsage.currentUsage.text') }}
|
|
||||||
</n8n-text>
|
|
||||||
</template>
|
|
||||||
<template #count>
|
|
||||||
<n8n-text size="small" :bold="true" color="warning">
|
|
||||||
{{
|
|
||||||
locale.baseText('executionUsage.currentUsage.count', {
|
|
||||||
adjustToNumber: daysLeftOnTrial || 0,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</n8n-text>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="!trialHasExecutionsLeft" :class="$style.usageText">
|
|
||||||
<n8n-text size="small">
|
|
||||||
{{ locale.baseText('executionUsage.ranOutOfExecutions.text') }}
|
|
||||||
</n8n-text>
|
|
||||||
</div>
|
|
||||||
<div v-if="!isTrialExpired" :class="$style.usageCounter">
|
|
||||||
<div :class="$style.progressBarDiv">
|
|
||||||
<progress
|
|
||||||
:class="[
|
|
||||||
trialHasExecutionsLeft ? $style.progressBarSuccess : $style.progressBarDanger,
|
|
||||||
$style.progressBar,
|
|
||||||
]"
|
|
||||||
:value="currentExecutionsWithThreshold"
|
|
||||||
:max="maxExecutions"
|
|
||||||
></progress>
|
|
||||||
</div>
|
|
||||||
<div :class="$style.executionsCountSection">
|
|
||||||
<n8n-text size="xsmall" :color="trialHasExecutionsLeft ? 'text-dark' : 'danger'">
|
|
||||||
{{ currentExecutions }}/{{ maxExecutions }}
|
|
||||||
</n8n-text>
|
|
||||||
<n8n-text size="xsmall" :color="trialHasExecutionsLeft ? 'text-dark' : 'danger'">{{
|
|
||||||
locale.baseText('executionUsage.label.executions')
|
|
||||||
}}</n8n-text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :class="$style.upgradeButtonSection">
|
|
||||||
<n8n-button
|
|
||||||
:label="locale.baseText('executionUsage.button.upgrade')"
|
|
||||||
size="xmini"
|
|
||||||
icon="gem"
|
|
||||||
type="success"
|
|
||||||
:block="true"
|
|
||||||
@click="onUpgradeClicked"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { i18n as locale } from '@/plugins/i18n';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import type { CloudPlanAndUsageData } from '@/Interface';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
|
|
||||||
const PROGRESS_BAR_MINIMUM_THRESHOLD = 8;
|
|
||||||
|
|
||||||
const props = defineProps<{ cloudPlanData: CloudPlanAndUsageData | null }>();
|
|
||||||
|
|
||||||
const now = DateTime.utc();
|
|
||||||
|
|
||||||
const daysLeftOnTrial = computed(() => {
|
|
||||||
const { days = 0 } = getPlanExpirationDate().diff(now, ['days']).toObject();
|
|
||||||
return Math.ceil(days);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isTrialExpired = computed(() => {
|
|
||||||
if (!props.cloudPlanData?.expirationDate) return false;
|
|
||||||
const trialEndsAt = DateTime.fromISO(props.cloudPlanData.expirationDate);
|
|
||||||
return now.toMillis() > trialEndsAt.toMillis();
|
|
||||||
});
|
|
||||||
|
|
||||||
const getPlanExpirationDate = () => DateTime.fromISO(props?.cloudPlanData?.expirationDate ?? '');
|
|
||||||
|
|
||||||
const trialHasExecutionsLeft = computed(() => {
|
|
||||||
if (!props.cloudPlanData?.usage) return 0;
|
|
||||||
return props.cloudPlanData.usage.executions < props.cloudPlanData.monthlyExecutionsLimit;
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentExecutions = computed(() => {
|
|
||||||
if (!props.cloudPlanData?.usage) return 0;
|
|
||||||
const usedExecutions = props.cloudPlanData.usage.executions;
|
|
||||||
const executionsQuota = props.cloudPlanData.monthlyExecutionsLimit;
|
|
||||||
return usedExecutions > executionsQuota ? executionsQuota : usedExecutions;
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentExecutionsWithThreshold = computed(() => {
|
|
||||||
if (!props.cloudPlanData?.usage) return 0;
|
|
||||||
const usedExecutions = props.cloudPlanData.usage.executions;
|
|
||||||
const executionsQuota = props.cloudPlanData.monthlyExecutionsLimit;
|
|
||||||
const threshold = (PROGRESS_BAR_MINIMUM_THRESHOLD * executionsQuota) / 100;
|
|
||||||
return usedExecutions < threshold ? threshold : usedExecutions;
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxExecutions = computed(() => {
|
|
||||||
if (!props.cloudPlanData?.monthlyExecutionsLimit) return 0;
|
|
||||||
return props.cloudPlanData.monthlyExecutionsLimit;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onUpgradeClicked = () => {
|
|
||||||
void useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style module lang="scss">
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--color-background-light);
|
|
||||||
border: var(--border-base);
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressBarDiv {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressBar {
|
|
||||||
width: 62.4px;
|
|
||||||
border: 0;
|
|
||||||
height: 5px;
|
|
||||||
border-radius: 20px;
|
|
||||||
background-color: var(--color-foreground-base);
|
|
||||||
}
|
|
||||||
.progressBar::-webkit-progress-bar {
|
|
||||||
width: 62.4px;
|
|
||||||
border: 0;
|
|
||||||
height: 5px;
|
|
||||||
border-radius: 20px;
|
|
||||||
background-color: var(--color-foreground-base);
|
|
||||||
}
|
|
||||||
.progressBar::-moz-progress-bar {
|
|
||||||
width: 62.4px;
|
|
||||||
border: 0;
|
|
||||||
height: 5px;
|
|
||||||
border-radius: 20px;
|
|
||||||
background-color: var(--color-foreground-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressBarSuccess::-moz-progress-bar {
|
|
||||||
background: var(--color-foreground-xdark);
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressBarSuccess::-webkit-progress-value {
|
|
||||||
background: var(--color-foreground-xdark);
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressBarDanger::-webkit-progress-value {
|
|
||||||
background: var(--color-danger);
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressBarDanger::-moz-progress-bar {
|
|
||||||
background: var(--color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.usageText {
|
|
||||||
margin-left: var(--spacing-s);
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
margin-top: var(--spacing-xs);
|
|
||||||
line-height: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.usageCounter {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: var(--spacing-2xs);
|
|
||||||
font-size: var(--font-size-3xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.danger {
|
|
||||||
color: var(--color-danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.executionsCountSection {
|
|
||||||
margin-left: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upgradeButtonSection {
|
|
||||||
margin: var(--spacing-s);
|
|
||||||
}
|
|
||||||
</style>
|
|
Loading…
Reference in a new issue