diff --git a/packages/backend/src/graphql/queries/get-executions.js b/packages/backend/src/graphql/queries/get-executions.js deleted file mode 100644 index d19e6420..00000000 --- a/packages/backend/src/graphql/queries/get-executions.js +++ /dev/null @@ -1,70 +0,0 @@ -import { raw } from 'objection'; -import { DateTime } from 'luxon'; -import Execution from '../../models/execution.js'; -import paginate from '../../helpers/pagination.js'; - -const getExecutions = async (_parent, params, context) => { - const conditions = context.currentUser.can('read', 'Execution'); - - const filters = params.filters; - - const userExecutions = context.currentUser.$relatedQuery('executions'); - const allExecutions = Execution.query(); - const executionBaseQuery = conditions.isCreator - ? userExecutions - : allExecutions; - - const selectStatusStatement = ` - case - when count(*) filter (where execution_steps.status = 'failure') > 0 - then 'failure' - else 'success' - end - as status - `; - - const executions = executionBaseQuery - .clone() - .joinRelated('executionSteps as execution_steps') - .select('executions.*', raw(selectStatusStatement)) - .groupBy('executions.id') - .orderBy('created_at', 'desc'); - - const computedExecutions = Execution.query() - .with('executions', executions) - .withSoftDeleted() - .withGraphFetched({ - flow: { - steps: true, - }, - }); - - if (filters?.flowId) { - computedExecutions.where('executions.flow_id', filters.flowId); - } - - if (filters?.status) { - computedExecutions.where('executions.status', filters.status); - } - - if (filters?.createdAt) { - const createdAtFilter = filters.createdAt; - if (createdAtFilter.from) { - const isoFromDateTime = DateTime.fromMillis( - parseInt(createdAtFilter.from, 10) - ).toISO(); - computedExecutions.where('executions.created_at', '>=', isoFromDateTime); - } - - if (createdAtFilter.to) { - const isoToDateTime = DateTime.fromMillis( - parseInt(createdAtFilter.to, 10) - ).toISO(); - computedExecutions.where('executions.created_at', '<=', isoToDateTime); - } - } - - return paginate(computedExecutions, params.limit, params.offset); -}; - -export default getExecutions; diff --git a/packages/backend/src/graphql/queries/get-executions.test.js b/packages/backend/src/graphql/queries/get-executions.test.js deleted file mode 100644 index 9f57b6f9..00000000 --- a/packages/backend/src/graphql/queries/get-executions.test.js +++ /dev/null @@ -1,472 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import request from 'supertest'; -import app from '../../app'; -import appConfig from '../../config/app'; -import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id'; -import { createRole } from '../../../test/factories/role'; -import { createPermission } from '../../../test/factories/permission'; -import { createUser } from '../../../test/factories/user'; -import { createFlow } from '../../../test/factories/flow'; -import { createStep } from '../../../test/factories/step'; -import { createExecution } from '../../../test/factories/execution'; -import { createExecutionStep } from '../../../test/factories/execution-step'; - -describe('graphQL getExecutions query', () => { - const query = ` - query { - getExecutions(limit: 10, offset: 0) { - pageInfo { - currentPage - totalPages - } - edges { - node { - id - testRun - createdAt - updatedAt - status - flow { - id - name - active - steps { - iconUrl - } - } - } - } - } - } - `; - - describe('and without correct permissions', () => { - it('should throw not authorized error', async () => { - const userWithoutPermissions = await createUser(); - const token = createAuthTokenByUserId(userWithoutPermissions.id); - - const response = await request(app) - .post('/graphql') - .set('Authorization', token) - .send({ query }) - .expect(200); - - expect(response.body.errors).toBeDefined(); - expect(response.body.errors[0].message).toEqual('Not authorized!'); - }); - }); - - describe('and with correct permission', () => { - let role, - currentUser, - anotherUser, - token, - flowOne, - stepOneForFlowOne, - stepTwoForFlowOne, - executionOne, - flowTwo, - stepOneForFlowTwo, - stepTwoForFlowTwo, - executionTwo, - flowThree, - stepOneForFlowThree, - stepTwoForFlowThree, - executionThree, - expectedResponseForExecutionOne, - expectedResponseForExecutionTwo, - expectedResponseForExecutionThree; - - beforeEach(async () => { - role = await createRole({ - key: 'sample', - name: 'sample', - }); - - currentUser = await createUser({ - roleId: role.id, - fullName: 'Current User', - }); - - anotherUser = await createUser(); - - token = createAuthTokenByUserId(currentUser.id); - - flowOne = await createFlow({ - userId: currentUser.id, - }); - - stepOneForFlowOne = await createStep({ - flowId: flowOne.id, - }); - - stepTwoForFlowOne = await createStep({ - flowId: flowOne.id, - }); - - executionOne = await createExecution({ - flowId: flowOne.id, - }); - - await createExecutionStep({ - executionId: executionOne.id, - stepId: stepOneForFlowOne.id, - status: 'success', - }); - - await createExecutionStep({ - executionId: executionOne.id, - stepId: stepTwoForFlowOne.id, - status: 'success', - }); - - flowTwo = await createFlow({ - userId: currentUser.id, - }); - - stepOneForFlowTwo = await createStep({ - flowId: flowTwo.id, - }); - - stepTwoForFlowTwo = await createStep({ - flowId: flowTwo.id, - }); - - executionTwo = await createExecution({ - flowId: flowTwo.id, - }); - - await createExecutionStep({ - executionId: executionTwo.id, - stepId: stepOneForFlowTwo.id, - status: 'success', - }); - - await createExecutionStep({ - executionId: executionTwo.id, - stepId: stepTwoForFlowTwo.id, - status: 'failure', - }); - - flowThree = await createFlow({ - userId: anotherUser.id, - }); - - stepOneForFlowThree = await createStep({ - flowId: flowThree.id, - }); - - stepTwoForFlowThree = await createStep({ - flowId: flowThree.id, - }); - - executionThree = await createExecution({ - flowId: flowThree.id, - }); - - await createExecutionStep({ - executionId: executionThree.id, - stepId: stepOneForFlowThree.id, - status: 'success', - }); - - await createExecutionStep({ - executionId: executionThree.id, - stepId: stepTwoForFlowThree.id, - status: 'failure', - }); - - expectedResponseForExecutionOne = { - node: { - createdAt: executionOne.createdAt.getTime().toString(), - flow: { - active: flowOne.active, - id: flowOne.id, - name: flowOne.name, - steps: [ - { - iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowOne.appKey}/assets/favicon.svg`, - }, - { - iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowOne.appKey}/assets/favicon.svg`, - }, - ], - }, - id: executionOne.id, - status: 'success', - testRun: executionOne.testRun, - updatedAt: executionOne.updatedAt.getTime().toString(), - }, - }; - - expectedResponseForExecutionTwo = { - node: { - createdAt: executionTwo.createdAt.getTime().toString(), - flow: { - active: flowTwo.active, - id: flowTwo.id, - name: flowTwo.name, - steps: [ - { - iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`, - }, - { - iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowTwo.appKey}/assets/favicon.svg`, - }, - ], - }, - id: executionTwo.id, - status: 'failure', - testRun: executionTwo.testRun, - updatedAt: executionTwo.updatedAt.getTime().toString(), - }, - }; - - expectedResponseForExecutionThree = { - node: { - createdAt: executionThree.createdAt.getTime().toString(), - flow: { - active: flowThree.active, - id: flowThree.id, - name: flowThree.name, - steps: [ - { - iconUrl: `${appConfig.baseUrl}/apps/${stepOneForFlowThree.appKey}/assets/favicon.svg`, - }, - { - iconUrl: `${appConfig.baseUrl}/apps/${stepTwoForFlowThree.appKey}/assets/favicon.svg`, - }, - ], - }, - id: executionThree.id, - status: 'failure', - testRun: executionThree.testRun, - updatedAt: executionThree.updatedAt.getTime().toString(), - }, - }; - }); - - describe('and with isCreator condition', () => { - beforeEach(async () => { - await createPermission({ - action: 'read', - subject: 'Execution', - roleId: role.id, - conditions: ['isCreator'], - }); - }); - - it('should return executions data of the current user', async () => { - const response = await request(app) - .post('/graphql') - .set('Authorization', token) - .send({ query }) - .expect(200); - - const expectedResponsePayload = { - data: { - getExecutions: { - edges: [ - expectedResponseForExecutionTwo, - expectedResponseForExecutionOne, - ], - pageInfo: { currentPage: 1, totalPages: 1 }, - }, - }, - }; - - expect(response.body).toEqual(expectedResponsePayload); - }); - }); - - describe('and without isCreator condition', () => { - beforeEach(async () => { - await createPermission({ - action: 'read', - subject: 'Execution', - roleId: role.id, - conditions: [], - }); - }); - - it('should return executions data of all users', async () => { - const response = await request(app) - .post('/graphql') - .set('Authorization', token) - .send({ query }) - .expect(200); - - const expectedResponsePayload = { - data: { - getExecutions: { - edges: [ - expectedResponseForExecutionThree, - expectedResponseForExecutionTwo, - expectedResponseForExecutionOne, - ], - pageInfo: { currentPage: 1, totalPages: 1 }, - }, - }, - }; - - expect(response.body).toEqual(expectedResponsePayload); - }); - }); - - describe('and with filters', () => { - beforeEach(async () => { - await createPermission({ - action: 'read', - subject: 'Execution', - roleId: role.id, - conditions: [], - }); - }); - - it('should return executions data for the specified flow', async () => { - const query = ` - query { - getExecutions(limit: 10, offset: 0, filters: { flowId: "${flowOne.id}" }) { - pageInfo { - currentPage - totalPages - } - edges { - node { - id - testRun - createdAt - updatedAt - status - flow { - id - name - active - steps { - iconUrl - } - } - } - } - } - } - `; - - const response = await request(app) - .post('/graphql') - .set('Authorization', token) - .send({ query }) - .expect(200); - - const expectedResponsePayload = { - data: { - getExecutions: { - edges: [expectedResponseForExecutionOne], - pageInfo: { currentPage: 1, totalPages: 1 }, - }, - }, - }; - - expect(response.body).toEqual(expectedResponsePayload); - }); - - it('should return only executions data with success status', async () => { - const query = ` - query { - getExecutions(limit: 10, offset: 0, filters: { status: "success" }) { - pageInfo { - currentPage - totalPages - } - edges { - node { - id - testRun - createdAt - updatedAt - status - flow { - id - name - active - steps { - iconUrl - } - } - } - } - } - } - `; - - const response = await request(app) - .post('/graphql') - .set('Authorization', token) - .send({ query }) - .expect(200); - - const expectedResponsePayload = { - data: { - getExecutions: { - edges: [expectedResponseForExecutionOne], - pageInfo: { currentPage: 1, totalPages: 1 }, - }, - }, - }; - - expect(response.body).toEqual(expectedResponsePayload); - }); - - it('should return only executions data within date range', async () => { - const createdAtFrom = executionOne.createdAt.getTime().toString(); - - const createdAtTo = executionOne.createdAt.getTime().toString(); - - const query = ` - query { - getExecutions(limit: 10, offset: 0, filters: { createdAt: { from: "${createdAtFrom}", to: "${createdAtTo}" }}) { - pageInfo { - currentPage - totalPages - } - edges { - node { - id - testRun - createdAt - updatedAt - status - flow { - id - name - active - steps { - iconUrl - } - } - } - } - } - } - `; - - const response = await request(app) - .post('/graphql') - .set('Authorization', token) - .send({ query }) - .expect(200); - - const expectedResponsePayload = { - data: { - getExecutions: { - edges: [expectedResponseForExecutionOne], - pageInfo: { currentPage: 1, totalPages: 1 }, - }, - }, - }; - - expect(response.body).toEqual(expectedResponsePayload); - }); - }); - }); -}); diff --git a/packages/backend/src/graphql/query-resolvers.js b/packages/backend/src/graphql/query-resolvers.js index 58b63077..1dbd0561 100644 --- a/packages/backend/src/graphql/query-resolvers.js +++ b/packages/backend/src/graphql/query-resolvers.js @@ -7,7 +7,6 @@ import getConnectedApps from './queries/get-connected-apps.js'; import getCurrentUser from './queries/get-current-user.js'; import getDynamicData from './queries/get-dynamic-data.js'; import getDynamicFields from './queries/get-dynamic-fields.js'; -import getExecutions from './queries/get-executions.js'; import getFlow from './queries/get-flow.js'; import getFlows from './queries/get-flows.js'; import getInvoices from './queries/get-invoices.ee.js'; @@ -38,7 +37,6 @@ const queryResolvers = { getCurrentUser, getDynamicData, getDynamicFields, - getExecutions, getFlow, getFlows, getInvoices, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 2ee3c650..d157fe23 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -13,11 +13,6 @@ type Query { name: String ): FlowConnection getStepWithTestExecutions(stepId: String!): [Step] - getExecutions( - limit: Int! - offset: Int! - filters: ExecutionFiltersInput - ): ExecutionConnection getDynamicData( stepId: String! key: String! @@ -301,15 +296,6 @@ type Flow { status: FlowStatus } -type Execution { - id: String - testRun: Boolean - createdAt: String - updatedAt: String - status: String - flow: Flow -} - type SamlAuthProvider { id: String name: String @@ -609,19 +595,10 @@ type PageInfo { totalPages: Int! } -type ExecutionEdge { - node: Execution -} - type ExecutionStepEdge { node: ExecutionStep } -type ExecutionConnection { - edges: [ExecutionEdge] - pageInfo: PageInfo -} - type ExecutionStepConnection { edges: [ExecutionStepEdge] pageInfo: PageInfo @@ -781,17 +758,6 @@ type Notification { description: String } -input ExecutionCreatedAtFilterInput { - from: String - to: String -} - -input ExecutionFiltersInput { - flowId: String - createdAt: ExecutionCreatedAtFilterInput - status: String -} - schema { query: Query mutation: Mutation diff --git a/packages/web/src/graphql/queries/get-executions.js b/packages/web/src/graphql/queries/get-executions.js deleted file mode 100644 index b4f8a7e9..00000000 --- a/packages/web/src/graphql/queries/get-executions.js +++ /dev/null @@ -1,28 +0,0 @@ -import { gql } from '@apollo/client'; -export const GET_EXECUTIONS = gql` - query GetExecutions($limit: Int!, $offset: Int!) { - getExecutions(limit: $limit, offset: $offset) { - pageInfo { - currentPage - totalPages - } - edges { - node { - id - testRun - createdAt - updatedAt - status - flow { - id - name - active - steps { - iconUrl - } - } - } - } - } - } -`; diff --git a/packages/web/src/hooks/useExecutions.js b/packages/web/src/hooks/useExecutions.js new file mode 100644 index 00000000..a2a964d5 --- /dev/null +++ b/packages/web/src/hooks/useExecutions.js @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useExecutions({ page }, { refetchInterval } = {}) { + const query = useQuery({ + queryKey: ['executions', page], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/executions`, { + params: { + page, + }, + signal, + }); + + return data; + }, + refetchInterval, + }); + + return query; +} diff --git a/packages/web/src/pages/Executions/index.jsx b/packages/web/src/pages/Executions/index.jsx index b323a0f4..5ca6ea12 100644 --- a/packages/web/src/pages/Executions/index.jsx +++ b/packages/web/src/pages/Executions/index.jsx @@ -1,39 +1,33 @@ import * as React from 'react'; import { Link, useSearchParams } from 'react-router-dom'; -import { useQuery } from '@apollo/client'; import Box from '@mui/material/Box'; import Grid from '@mui/material/Grid'; import CircularProgress from '@mui/material/CircularProgress'; import Divider from '@mui/material/Divider'; import Pagination from '@mui/material/Pagination'; import PaginationItem from '@mui/material/PaginationItem'; + import NoResultFound from 'components/NoResultFound'; import ExecutionRow from 'components/ExecutionRow'; import Container from 'components/Container'; import PageTitle from 'components/PageTitle'; import useFormatMessage from 'hooks/useFormatMessage'; -import { GET_EXECUTIONS } from 'graphql/queries/get-executions'; -const EXECUTION_PER_PAGE = 10; -const getLimitAndOffset = (page) => ({ - limit: EXECUTION_PER_PAGE, - offset: (page - 1) * EXECUTION_PER_PAGE, -}); +import useExecutions from 'hooks/useExecutions'; + export default function Executions() { const formatMessage = useFormatMessage(); const [searchParams, setSearchParams] = useSearchParams(); const page = parseInt(searchParams.get('page') || '', 10) || 1; - const { data, refetch, loading } = useQuery(GET_EXECUTIONS, { - variables: getLimitAndOffset(page), - fetchPolicy: 'cache-and-network', - pollInterval: 5000, - }); - const getExecutions = data?.getExecutions || {}; - const { pageInfo, edges } = getExecutions; - React.useEffect(() => { - refetch(getLimitAndOffset(page)); - }, [refetch, page]); - const executions = edges?.map(({ node }) => node); + + const { data, isLoading: isExecutionsLoading } = useExecutions( + { page: page }, + { refetchInterval: 5000 }, + ); + + const { data: executions, meta: pageInfo } = data || {}; + const hasExecutions = executions?.length; + return ( @@ -52,18 +46,18 @@ export default function Executions() { - {loading && ( + {isExecutionsLoading && ( )} - {!loading && !hasExecutions && ( + {!isExecutionsLoading && !hasExecutions && ( )} - {!loading && + {!isExecutionsLoading && executions?.map((execution) => ( ))}