mirror of
				https://github.com/datahub-project/datahub.git
				synced 2025-11-03 20:27:50 +00:00 
			
		
		
		
	feat(ingestion): add rollback button (#13677)
This commit is contained in:
		
							parent
							
								
									65ae48e774
								
							
						
					
					
						commit
						93aa64200a
					
				@ -110,6 +110,7 @@ import com.linkedin.datahub.graphql.resolvers.incident.UpdateIncidentStatusResol
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.ingest.execution.CancelIngestionExecutionRequestResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.ingest.execution.CreateIngestionExecutionRequestResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.ingest.execution.CreateTestConnectionRequestResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.ingest.execution.GetLatestSuccessfulExecutionRequestResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.ingest.execution.IngestionSourceExecutionRequestsResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.ingest.execution.ListExecutionRequestsResolver;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.ingest.execution.RollbackIngestionResolver;
 | 
			
		||||
@ -3194,7 +3195,11 @@ public class GmsGraphQLEngine {
 | 
			
		||||
                              return ingestionSource.getPlatform() != null
 | 
			
		||||
                                  ? ingestionSource.getPlatform().getUrn()
 | 
			
		||||
                                  : null;
 | 
			
		||||
                            })))
 | 
			
		||||
                            }))
 | 
			
		||||
                    .dataFetcher(
 | 
			
		||||
                        "latestSuccessfulExecution",
 | 
			
		||||
                        new GetLatestSuccessfulExecutionRequestResolver(
 | 
			
		||||
                            this.entityClient, this.executionRequestType)))
 | 
			
		||||
        .type(
 | 
			
		||||
            "ListIngestionSourcesResult",
 | 
			
		||||
            typeWiring ->
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,112 @@
 | 
			
		||||
package com.linkedin.datahub.graphql.resolvers.ingest.execution;
 | 
			
		||||
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.ExecutionRequest;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.FacetFilterInput;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.FilterOperator;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.IngestionSource;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.ResolverUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.ingestion.ExecutionRequestType;
 | 
			
		||||
import com.linkedin.entity.client.EntityClient;
 | 
			
		||||
import com.linkedin.metadata.Constants;
 | 
			
		||||
import com.linkedin.metadata.query.filter.SortCriterion;
 | 
			
		||||
import com.linkedin.metadata.query.filter.SortOrder;
 | 
			
		||||
import com.linkedin.metadata.search.SearchEntity;
 | 
			
		||||
import com.linkedin.metadata.search.SearchResult;
 | 
			
		||||
import graphql.execution.DataFetcherResult;
 | 
			
		||||
import graphql.schema.DataFetcher;
 | 
			
		||||
import graphql.schema.DataFetchingEnvironment;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.CompletableFuture;
 | 
			
		||||
import javax.annotation.Nonnull;
 | 
			
		||||
 | 
			
		||||
/** Returns the latest successful execution request by ingestion source */
 | 
			
		||||
public class GetLatestSuccessfulExecutionRequestResolver
 | 
			
		||||
    implements DataFetcher<CompletableFuture<ExecutionRequest>> {
 | 
			
		||||
 | 
			
		||||
  private final EntityClient _entityClient;
 | 
			
		||||
  private final ExecutionRequestType _executionRequestType;
 | 
			
		||||
 | 
			
		||||
  private final String INGESTION_SOURCE_FIELD = "ingestionSource";
 | 
			
		||||
  private final String RESULT_STATUS_FIELD = "executionResultStatus";
 | 
			
		||||
  private final String RESULT_REQUEST_TIME_MS_FIELD = "requestTimeMs";
 | 
			
		||||
  private final String STATUS_SUCCESS = "SUCCESS";
 | 
			
		||||
 | 
			
		||||
  public GetLatestSuccessfulExecutionRequestResolver(
 | 
			
		||||
      final EntityClient entityClient, final ExecutionRequestType executionRequestType) {
 | 
			
		||||
    _entityClient = entityClient;
 | 
			
		||||
    _executionRequestType = executionRequestType;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Override
 | 
			
		||||
  public CompletableFuture<ExecutionRequest> get(final DataFetchingEnvironment environment)
 | 
			
		||||
      throws Exception {
 | 
			
		||||
    final QueryContext context = environment.getContext();
 | 
			
		||||
    final String ingestionSourceUrn = ((IngestionSource) environment.getSource()).getUrn();
 | 
			
		||||
 | 
			
		||||
    return GraphQLConcurrencyUtils.supplyAsync(
 | 
			
		||||
        () -> {
 | 
			
		||||
          try {
 | 
			
		||||
            final Urn executionRequestUrn =
 | 
			
		||||
                getLatestSuccessfulExecutionRequestUrn(ingestionSourceUrn, context);
 | 
			
		||||
 | 
			
		||||
            if (executionRequestUrn == null) {
 | 
			
		||||
              return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Optional<DataFetcherResult<ExecutionRequest>> resp =
 | 
			
		||||
                _executionRequestType
 | 
			
		||||
                    .batchLoad(List.of(executionRequestUrn.toString()), context)
 | 
			
		||||
                    .stream()
 | 
			
		||||
                    .findFirst();
 | 
			
		||||
            return resp.map(DataFetcherResult::getData).orElse(null);
 | 
			
		||||
          } catch (Exception e) {
 | 
			
		||||
            throw new RuntimeException(
 | 
			
		||||
                String.format(
 | 
			
		||||
                    "Failed to get latest successful execution request by source: %s",
 | 
			
		||||
                    ingestionSourceUrn),
 | 
			
		||||
                e);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        this.getClass().getSimpleName(),
 | 
			
		||||
        "get");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private Urn getLatestSuccessfulExecutionRequestUrn(
 | 
			
		||||
      @Nonnull final String ingestionSourceUrn, @Nonnull QueryContext context) throws Exception {
 | 
			
		||||
    final SearchResult gmsResult =
 | 
			
		||||
        _entityClient.filter(
 | 
			
		||||
            context.getOperationContext(),
 | 
			
		||||
            Constants.EXECUTION_REQUEST_ENTITY_NAME,
 | 
			
		||||
            Objects.requireNonNull(
 | 
			
		||||
                ResolverUtils.buildFilter(
 | 
			
		||||
                    List.of(
 | 
			
		||||
                        new FacetFilterInput(
 | 
			
		||||
                            INGESTION_SOURCE_FIELD,
 | 
			
		||||
                            null,
 | 
			
		||||
                            List.of(ingestionSourceUrn),
 | 
			
		||||
                            false,
 | 
			
		||||
                            FilterOperator.EQUAL),
 | 
			
		||||
                        new FacetFilterInput(
 | 
			
		||||
                            RESULT_STATUS_FIELD,
 | 
			
		||||
                            null,
 | 
			
		||||
                            List.of(STATUS_SUCCESS),
 | 
			
		||||
                            false,
 | 
			
		||||
                            FilterOperator.EQUAL)),
 | 
			
		||||
                    Collections.emptyList())),
 | 
			
		||||
            Collections.singletonList(
 | 
			
		||||
                new SortCriterion()
 | 
			
		||||
                    .setField(RESULT_REQUEST_TIME_MS_FIELD)
 | 
			
		||||
                    .setOrder(SortOrder.DESCENDING)),
 | 
			
		||||
            0,
 | 
			
		||||
            1);
 | 
			
		||||
    Optional<SearchEntity> optionalSearchEntity = gmsResult.getEntities().stream().findFirst();
 | 
			
		||||
 | 
			
		||||
    return optionalSearchEntity.map(SearchEntity::getEntity).orElse(null);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -503,6 +503,11 @@ type IngestionSource {
 | 
			
		||||
  Ownership metadata of the ingestion source
 | 
			
		||||
  """
 | 
			
		||||
  ownership: Ownership
 | 
			
		||||
 | 
			
		||||
  """
 | 
			
		||||
  The latest successful execution request for this source
 | 
			
		||||
  """
 | 
			
		||||
  latestSuccessfulExecution: ExecutionRequest
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,149 @@
 | 
			
		||||
package com.linkedin.datahub.graphql.resolvers.ingest.execution;
 | 
			
		||||
 | 
			
		||||
import static com.linkedin.datahub.graphql.resolvers.ingest.IngestTestUtils.getMockAllowContext;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.any;
 | 
			
		||||
import static org.testng.Assert.assertEquals;
 | 
			
		||||
import static org.testng.Assert.assertNull;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableSet;
 | 
			
		||||
import com.linkedin.common.urn.Urn;
 | 
			
		||||
import com.linkedin.datahub.graphql.QueryContext;
 | 
			
		||||
import com.linkedin.datahub.graphql.generated.*;
 | 
			
		||||
import com.linkedin.datahub.graphql.resolvers.ResolverUtils;
 | 
			
		||||
import com.linkedin.datahub.graphql.types.ingestion.ExecutionRequestType;
 | 
			
		||||
import com.linkedin.entity.client.EntityClient;
 | 
			
		||||
import com.linkedin.metadata.Constants;
 | 
			
		||||
import com.linkedin.metadata.query.filter.SortOrder;
 | 
			
		||||
import com.linkedin.metadata.search.SearchEntity;
 | 
			
		||||
import com.linkedin.metadata.search.SearchEntityArray;
 | 
			
		||||
import com.linkedin.metadata.search.SearchResult;
 | 
			
		||||
import graphql.execution.DataFetcherResult;
 | 
			
		||||
import graphql.schema.DataFetchingEnvironment;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import org.mockito.Mockito;
 | 
			
		||||
import org.slf4j.Logger;
 | 
			
		||||
import org.slf4j.LoggerFactory;
 | 
			
		||||
import org.testng.annotations.Test;
 | 
			
		||||
 | 
			
		||||
public class GetLatestSuccessfulExecutionRequestResolverTest {
 | 
			
		||||
 | 
			
		||||
  private static final String TEST_EXECUTION_REQUEST_URN = "urn:li:executionRequest:test-execution";
 | 
			
		||||
  private static final String TEST_INGESTION_SOURCE_URN =
 | 
			
		||||
      "urn:li:dataHubIngestionSource:test-source";
 | 
			
		||||
 | 
			
		||||
  private static final ListExecutionRequestsInput TEST_INPUT =
 | 
			
		||||
      new ListExecutionRequestsInput(0, 20, null, null, null, null);
 | 
			
		||||
  private static final Logger log =
 | 
			
		||||
      LoggerFactory.getLogger(GetLatestSuccessfulExecutionRequestResolverTest.class);
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void testGetSuccess() throws Exception {
 | 
			
		||||
    EntityClient mockEntityClient =
 | 
			
		||||
        getMockedEntityClient(
 | 
			
		||||
            new SearchResult()
 | 
			
		||||
                .setFrom(0)
 | 
			
		||||
                .setPageSize(1)
 | 
			
		||||
                .setNumEntities(1)
 | 
			
		||||
                .setEntities(
 | 
			
		||||
                    new SearchEntityArray(
 | 
			
		||||
                        ImmutableSet.of(
 | 
			
		||||
                            new SearchEntity()
 | 
			
		||||
                                .setEntity(Urn.createFromString(TEST_EXECUTION_REQUEST_URN))))));
 | 
			
		||||
    ExecutionRequestType mockExecutionRequestType = Mockito.mock(ExecutionRequestType.class);
 | 
			
		||||
    Mockito.when(
 | 
			
		||||
            mockExecutionRequestType.batchLoad(
 | 
			
		||||
                Mockito.eq(List.of(TEST_EXECUTION_REQUEST_URN)), any()))
 | 
			
		||||
        .thenReturn(
 | 
			
		||||
            List.of(
 | 
			
		||||
                DataFetcherResult.<ExecutionRequest>newResult()
 | 
			
		||||
                    .data(getTestExecutionRequest())
 | 
			
		||||
                    .build()));
 | 
			
		||||
    QueryContext mockContext = getMockAllowContext();
 | 
			
		||||
    DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
 | 
			
		||||
    Mockito.when(mockEnv.getSource()).thenReturn(getTestIngestionSource());
 | 
			
		||||
    Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
 | 
			
		||||
 | 
			
		||||
    GetLatestSuccessfulExecutionRequestResolver resolver =
 | 
			
		||||
        new GetLatestSuccessfulExecutionRequestResolver(mockEntityClient, mockExecutionRequestType);
 | 
			
		||||
    var result = resolver.get(mockEnv).get();
 | 
			
		||||
 | 
			
		||||
    assertEquals(result.getUrn(), TEST_EXECUTION_REQUEST_URN);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  public void testGetSuccessWithEmptyResponse() throws Exception {
 | 
			
		||||
    EntityClient mockEntityClient =
 | 
			
		||||
        getMockedEntityClient(
 | 
			
		||||
            new SearchResult()
 | 
			
		||||
                .setFrom(0)
 | 
			
		||||
                .setPageSize(1)
 | 
			
		||||
                .setNumEntities(0)
 | 
			
		||||
                .setEntities(new SearchEntityArray(ImmutableSet.of())));
 | 
			
		||||
    ExecutionRequestType mockExecutionRequestType = Mockito.mock(ExecutionRequestType.class);
 | 
			
		||||
    QueryContext mockContext = getMockAllowContext();
 | 
			
		||||
    DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
 | 
			
		||||
    Mockito.when(mockEnv.getSource()).thenReturn(getTestIngestionSource());
 | 
			
		||||
    Mockito.when(mockEnv.getContext()).thenReturn(mockContext);
 | 
			
		||||
 | 
			
		||||
    GetLatestSuccessfulExecutionRequestResolver resolver =
 | 
			
		||||
        new GetLatestSuccessfulExecutionRequestResolver(mockEntityClient, mockExecutionRequestType);
 | 
			
		||||
    var result = resolver.get(mockEnv).get();
 | 
			
		||||
 | 
			
		||||
    assertNull(result);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private ExecutionRequest getTestExecutionRequest() {
 | 
			
		||||
    return new ExecutionRequest(
 | 
			
		||||
        TEST_EXECUTION_REQUEST_URN,
 | 
			
		||||
        EntityType.EXECUTION_REQUEST,
 | 
			
		||||
        "testId",
 | 
			
		||||
        new ExecutionRequestInput("task", null, null, 0L, null, null, "default"),
 | 
			
		||||
        null,
 | 
			
		||||
        null,
 | 
			
		||||
        null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private IngestionSource getTestIngestionSource() {
 | 
			
		||||
    IngestionSource ingestionSource = new IngestionSource();
 | 
			
		||||
    ingestionSource.setUrn(TEST_INGESTION_SOURCE_URN);
 | 
			
		||||
    return ingestionSource;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private EntityClient getMockedEntityClient(SearchResult filterSearchResult) throws Exception {
 | 
			
		||||
    EntityClient mockClient = Mockito.mock(EntityClient.class);
 | 
			
		||||
 | 
			
		||||
    Mockito.when(
 | 
			
		||||
            mockClient.filter(
 | 
			
		||||
                any(),
 | 
			
		||||
                Mockito.eq(Constants.EXECUTION_REQUEST_ENTITY_NAME),
 | 
			
		||||
                Mockito.eq(
 | 
			
		||||
                    Objects.requireNonNull(
 | 
			
		||||
                        ResolverUtils.buildFilter(
 | 
			
		||||
                            List.of(
 | 
			
		||||
                                new FacetFilterInput(
 | 
			
		||||
                                    "ingestionSource",
 | 
			
		||||
                                    null,
 | 
			
		||||
                                    List.of(TEST_INGESTION_SOURCE_URN),
 | 
			
		||||
                                    false,
 | 
			
		||||
                                    FilterOperator.EQUAL),
 | 
			
		||||
                                new FacetFilterInput(
 | 
			
		||||
                                    "executionResultStatus",
 | 
			
		||||
                                    null,
 | 
			
		||||
                                    List.of("SUCCESS"),
 | 
			
		||||
                                    false,
 | 
			
		||||
                                    FilterOperator.EQUAL)),
 | 
			
		||||
                            Collections.emptyList()))),
 | 
			
		||||
                Mockito.eq(
 | 
			
		||||
                    Collections.singletonList(
 | 
			
		||||
                        new com.linkedin.metadata.query.filter.SortCriterion()
 | 
			
		||||
                            .setField("requestTimeMs")
 | 
			
		||||
                            .setOrder(SortOrder.DESCENDING))),
 | 
			
		||||
                Mockito.eq(0),
 | 
			
		||||
                Mockito.eq(1)))
 | 
			
		||||
        .thenReturn(filterSearchResult);
 | 
			
		||||
 | 
			
		||||
    return mockClient;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { Pagination } from '@components';
 | 
			
		||||
import { message } from 'antd';
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
 | 
			
		||||
@ -14,7 +15,7 @@ import { Message } from '@app/shared/Message';
 | 
			
		||||
import { scrollToTop } from '@app/shared/searchUtils';
 | 
			
		||||
import usePagination from '@app/sharedV2/pagination/usePagination';
 | 
			
		||||
 | 
			
		||||
import { useListIngestionExecutionRequestsQuery } from '@graphql/ingestion.generated';
 | 
			
		||||
import { useListIngestionExecutionRequestsQuery, useRollbackIngestionMutation } from '@graphql/ingestion.generated';
 | 
			
		||||
import { ExecutionRequest } from '@types';
 | 
			
		||||
 | 
			
		||||
const SourceContainer = styled.div`
 | 
			
		||||
@ -78,6 +79,27 @@ export const ExecutionsTab = () => {
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const [rollbackIngestion] = useRollbackIngestionMutation();
 | 
			
		||||
 | 
			
		||||
    const handleRollbackExecution = useCallback(
 | 
			
		||||
        (runId: string) => {
 | 
			
		||||
            message.loading('Requesting rollback...');
 | 
			
		||||
 | 
			
		||||
            rollbackIngestion({ variables: { input: { runId } } })
 | 
			
		||||
                .then(() => {
 | 
			
		||||
                    setTimeout(() => {
 | 
			
		||||
                        message.destroy();
 | 
			
		||||
                        refetch();
 | 
			
		||||
                        message.success('Successfully requested ingestion rollback');
 | 
			
		||||
                    }, 2000);
 | 
			
		||||
                })
 | 
			
		||||
                .catch(() => {
 | 
			
		||||
                    message.error('Error requesting ingestion rollback');
 | 
			
		||||
                });
 | 
			
		||||
        },
 | 
			
		||||
        [refetch, rollbackIngestion],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const totalExecutionRequests = data?.listExecutionRequests?.total || 0;
 | 
			
		||||
    const executionRequests: ExecutionRequest[] = data?.listExecutionRequests?.executionRequests || [];
 | 
			
		||||
 | 
			
		||||
@ -117,6 +139,7 @@ export const ExecutionsTab = () => {
 | 
			
		||||
                                <ExecutionsTable
 | 
			
		||||
                                    executionRequests={executionRequests || []}
 | 
			
		||||
                                    setFocusExecutionUrn={setExecutionRequestUrnToView}
 | 
			
		||||
                                    handleRollback={handleRollbackExecution}
 | 
			
		||||
                                    loading={loading}
 | 
			
		||||
                                />
 | 
			
		||||
                            </TableContainer>
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Column, Table } from '@components';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import React, { useCallback, useState } from 'react';
 | 
			
		||||
import styled from 'styled-components/macro';
 | 
			
		||||
 | 
			
		||||
import { CLI_EXECUTOR_ID } from '@app/ingestV2/constants';
 | 
			
		||||
@ -11,6 +11,7 @@ import DateTimeColumn from '@app/ingestV2/shared/components/columns/DateTimeColu
 | 
			
		||||
import DurationColumn from '@app/ingestV2/shared/components/columns/DurationColumn';
 | 
			
		||||
import { StatusColumn } from '@app/ingestV2/shared/components/columns/StatusColumn';
 | 
			
		||||
import { getIngestionSourceStatus } from '@app/ingestV2/source/utils';
 | 
			
		||||
import { ConfirmationModal } from '@app/sharedV2/modals/ConfirmationModal';
 | 
			
		||||
 | 
			
		||||
import { ExecutionRequest } from '@types';
 | 
			
		||||
 | 
			
		||||
@ -22,9 +23,12 @@ interface Props {
 | 
			
		||||
    executionRequests: ExecutionRequest[];
 | 
			
		||||
    setFocusExecutionUrn: (urn: string) => void;
 | 
			
		||||
    loading?: boolean;
 | 
			
		||||
    handleRollback: (executionUrn: string) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function ExecutionsTable({ executionRequests, setFocusExecutionUrn, loading }: Props) {
 | 
			
		||||
export default function ExecutionsTable({ executionRequests, setFocusExecutionUrn, loading, handleRollback }: Props) {
 | 
			
		||||
    const [runIdOfRollbackConfirmation, setRunIdOfRollbackConfirmation] = useState<string | undefined>();
 | 
			
		||||
 | 
			
		||||
    const tableData: ExecutionRequestRecord[] = executionRequests.map((execution) => ({
 | 
			
		||||
        urn: execution.urn,
 | 
			
		||||
        name: execution?.source?.name,
 | 
			
		||||
@ -35,11 +39,15 @@ export default function ExecutionsTable({ executionRequests, setFocusExecutionUr
 | 
			
		||||
        startedAt: execution.result?.startTimeMs,
 | 
			
		||||
        duration: execution.result?.durationMs,
 | 
			
		||||
        status: getIngestionSourceStatus(execution.result),
 | 
			
		||||
        // TODO:: getting this field form backend is not implemented yet
 | 
			
		||||
        showRollback: false,
 | 
			
		||||
        showRollback: execution.source?.latestSuccessfulExecution?.urn === execution.urn,
 | 
			
		||||
        cliIngestion: execution.input.executorId === CLI_EXECUTOR_ID,
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    const handleConfirmRollback = useCallback(() => {
 | 
			
		||||
        if (runIdOfRollbackConfirmation) handleRollback(runIdOfRollbackConfirmation);
 | 
			
		||||
        setRunIdOfRollbackConfirmation(undefined);
 | 
			
		||||
    }, [handleRollback, runIdOfRollbackConfirmation]);
 | 
			
		||||
 | 
			
		||||
    const tableColumns: Column<ExecutionRequestRecord>[] = [
 | 
			
		||||
        {
 | 
			
		||||
            title: 'Name',
 | 
			
		||||
@ -76,10 +84,29 @@ export default function ExecutionsTable({ executionRequests, setFocusExecutionUr
 | 
			
		||||
        {
 | 
			
		||||
            title: '',
 | 
			
		||||
            key: 'actions',
 | 
			
		||||
            render: (record) => <ActionsColumn record={record} setFocusExecutionUrn={setFocusExecutionUrn} />,
 | 
			
		||||
            width: '50px',
 | 
			
		||||
            render: (record) => (
 | 
			
		||||
                <ActionsColumn
 | 
			
		||||
                    record={record}
 | 
			
		||||
                    setFocusExecutionUrn={setFocusExecutionUrn}
 | 
			
		||||
                    handleRollback={() => setRunIdOfRollbackConfirmation(record.id)}
 | 
			
		||||
                />
 | 
			
		||||
            ),
 | 
			
		||||
            width: '100px',
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return <StyledTable columns={tableColumns} data={tableData} isScrollable isLoading={loading} />;
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <StyledTable columns={tableColumns} data={tableData} isScrollable isLoading={loading} />
 | 
			
		||||
            <ConfirmationModal
 | 
			
		||||
                isOpen={!!runIdOfRollbackConfirmation}
 | 
			
		||||
                modalTitle="Confirm Rollback"
 | 
			
		||||
                modalText="Are you sure you want to continue? 
 | 
			
		||||
                    Rolling back this ingestion run will remove any new data ingested during the run. This may
 | 
			
		||||
                    exclude data that was previously extracted, but did not change during this run."
 | 
			
		||||
                handleConfirm={() => handleConfirmRollback()}
 | 
			
		||||
                handleClose={() => setRunIdOfRollbackConfirmation(undefined)}
 | 
			
		||||
            />
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,17 @@
 | 
			
		||||
import { Icon } from '@components';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
import { EXECUTION_REQUEST_STATUS_RUNNING } from '@app/ingestV2/executions/constants';
 | 
			
		||||
import { EXECUTION_REQUEST_STATUS_RUNNING, EXECUTION_REQUEST_STATUS_SUCCESS } from '@app/ingestV2/executions/constants';
 | 
			
		||||
import { ExecutionRequestRecord } from '@app/ingestV2/executions/types';
 | 
			
		||||
import BaseActionsColumn, { MenuItem } from '@app/ingestV2/shared/components/columns/BaseActionsColumn';
 | 
			
		||||
 | 
			
		||||
interface ActionsColumnProps {
 | 
			
		||||
    record: ExecutionRequestRecord;
 | 
			
		||||
    setFocusExecutionUrn: (urn: string) => void;
 | 
			
		||||
    handleRollback: (urn: string) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ActionsColumn({ record, setFocusExecutionUrn }: ActionsColumnProps) {
 | 
			
		||||
export function ActionsColumn({ record, setFocusExecutionUrn, handleRollback }: ActionsColumnProps) {
 | 
			
		||||
    const items = [
 | 
			
		||||
        {
 | 
			
		||||
            key: '0',
 | 
			
		||||
@ -42,5 +44,14 @@ export function ActionsColumn({ record, setFocusExecutionUrn }: ActionsColumnPro
 | 
			
		||||
            label: <MenuItem onClick={() => {}}>Cancel</MenuItem>,
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
    return <BaseActionsColumn dropdownItems={items} />;
 | 
			
		||||
    return (
 | 
			
		||||
        <BaseActionsColumn
 | 
			
		||||
            dropdownItems={items}
 | 
			
		||||
            extraActions={
 | 
			
		||||
                record.status === EXECUTION_REQUEST_STATUS_SUCCESS && record.showRollback ? (
 | 
			
		||||
                    <Icon icon="ArrowUUpLeft" source="phosphor" onClick={() => handleRollback(record.id)} />
 | 
			
		||||
                ) : null
 | 
			
		||||
            }
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1855,6 +1855,19 @@ fragment ingestionSourceFields on IngestionSource {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fragment minimalIngestionExecutionRequestFields on ExecutionRequest {
 | 
			
		||||
    urn
 | 
			
		||||
    id
 | 
			
		||||
    type
 | 
			
		||||
    input {
 | 
			
		||||
        requestedAt
 | 
			
		||||
        source {
 | 
			
		||||
            type
 | 
			
		||||
        }
 | 
			
		||||
        task
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fragment ingestionExecutionRequestFields on ExecutionRequest {
 | 
			
		||||
    urn
 | 
			
		||||
    id
 | 
			
		||||
 | 
			
		||||
@ -52,6 +52,9 @@ query listIngestionExecutionRequests($input: ListExecutionRequestsInput!) {
 | 
			
		||||
            ...ingestionExecutionRequestFields
 | 
			
		||||
            source {
 | 
			
		||||
                ...ingestionSourceFields
 | 
			
		||||
                latestSuccessfulExecution {
 | 
			
		||||
                    ...minimalIngestionExecutionRequestFields
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user