You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by wa...@apache.org on 2022/08/17 03:24:37 UTC

[incubator-devlake] branch release-v0.12 updated: fix: config-ui blueprints service pack 2.0 (#2630)

This is an automated email from the ASF dual-hosted git repository.

warren pushed a commit to branch release-v0.12
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/release-v0.12 by this push:
     new 808255bf fix: config-ui blueprints service pack 2.0 (#2630)
808255bf is described below

commit 808255bf9b30c7dc83a542f6d02766fb5a744838
Author: Julien Chinapen <ju...@merico.dev>
AuthorDate: Wed Aug 3 02:51:54 2022 -0400

    fix: config-ui blueprints service pack 2.0 (#2630)
    
    * fix: enhance stage task name text details
    
    * fix: cleanup mock objects from blueprint detail
    
    * chore: apply linting cleanups
    
    * feat: enable pipeline logfile download
    
    * fix: resolve lint dependencies with bp validate
    
    * fix: terminate regex match on repo name validation
---
 config-ui/package-lock.json                        |  11 +
 config-ui/package.json                             |   1 +
 .../src/components/blueprints/BlueprintsGrid.jsx   |   9 +-
 .../components/blueprints/DataEntitiesSelector.jsx |   6 +-
 .../blueprints/create-workflow/DataScopes.jsx      |   4 +
 .../create-workflow/DataTransformations.jsx        |   2 +
 .../src/components/pipelines/StageTaskName.jsx     |  26 +-
 config-ui/src/data/NullBlueprint.js                |   2 +-
 config-ui/src/data/Task.js                         |  66 +++
 config-ui/src/data/TestBlueprintDetail.js          | 379 ++++++++++++++
 config-ui/src/hooks/useBlueprintValidation.jsx     |   8 +-
 config-ui/src/hooks/usePipelineManager.jsx         |  23 +-
 .../src/pages/blueprints/blueprint-detail.jsx      | 583 ++-------------------
 .../src/pages/blueprints/create-blueprint.jsx      |   2 +
 config-ui/src/pages/blueprints/index.jsx           |   3 +
 config-ui/src/pages/configure/settings/github.jsx  |   2 +-
 16 files changed, 570 insertions(+), 557 deletions(-)

diff --git a/config-ui/package-lock.json b/config-ui/package-lock.json
index 476d942c..22f3cf35 100644
--- a/config-ui/package-lock.json
+++ b/config-ui/package-lock.json
@@ -21,6 +21,7 @@
         "dayjs": "^1.10.7",
         "dotenv": "^10.0.0",
         "dotenv-webpack": "^7.0.3",
+        "file-saver": "^2.0.5",
         "jetbrains-mono": "^1.0.6",
         "react": "17.0.2",
         "react-dom": "17.0.2",
@@ -10512,6 +10513,11 @@
         "webpack": "^4.0.0 || ^5.0.0"
       }
     },
+    "node_modules/file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+    },
     "node_modules/file-uri-to-path": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -33835,6 +33841,11 @@
         "schema-utils": "^3.0.0"
       }
     },
+    "file-saver": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
+      "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
+    },
     "file-uri-to-path": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
diff --git a/config-ui/package.json b/config-ui/package.json
index 235469bd..45d8b0bc 100644
--- a/config-ui/package.json
+++ b/config-ui/package.json
@@ -24,6 +24,7 @@
     "dayjs": "^1.10.7",
     "dotenv": "^10.0.0",
     "dotenv-webpack": "^7.0.3",
+    "file-saver": "^2.0.5",
     "jetbrains-mono": "^1.0.6",
     "react": "17.0.2",
     "react-dom": "17.0.2",
diff --git a/config-ui/src/components/blueprints/BlueprintsGrid.jsx b/config-ui/src/components/blueprints/BlueprintsGrid.jsx
index aa897be2..57048a4f 100644
--- a/config-ui/src/components/blueprints/BlueprintsGrid.jsx
+++ b/config-ui/src/components/blueprints/BlueprintsGrid.jsx
@@ -113,6 +113,13 @@ const BlueprintsGrid = (props) => {
           >
             Monthly
           </Button>
+          <Button
+            intent={activeFilterStatus === 'manual' ? Intent.PRIMARY : Intent.NONE}
+            active={activeFilterStatus === 'manual'}
+            onClick={() => onFilter('manual')}
+          >
+            Manual
+          </Button>
           <Button
             intent={activeFilterStatus === 'custom' ? Intent.PRIMARY : Intent.NONE}
             active={activeFilterStatus === 'custom'}
@@ -304,7 +311,7 @@ const BlueprintsGrid = (props) => {
                   }}
                 >
                   <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '0', padding: '10px' }}>
-                    <div style={{ letterSpacing: '1px', fontWeight: 800 }}>
+                    <div style={{ letterSpacing: '1px', fontWeight: 800, whiteSpace: 'nowrap' }}>
                       <Icon icon='bold' color={Colors.BLUE4} size={14} style={{ marginRight: '5px' }} /> BLUEPRINT ID {activeBlueprint?.id}
                       {isLoading && (
                         <span style={{ paddingLeft: '20px', fontWeight: 700, color: '#777777', fontSize: '11px' }}>
diff --git a/config-ui/src/components/blueprints/DataEntitiesSelector.jsx b/config-ui/src/components/blueprints/DataEntitiesSelector.jsx
index 68f9cac3..bc20eddc 100644
--- a/config-ui/src/components/blueprints/DataEntitiesSelector.jsx
+++ b/config-ui/src/components/blueprints/DataEntitiesSelector.jsx
@@ -41,6 +41,8 @@ import {
   Tag,
 } from '@blueprintjs/core'
 import { MultiSelect, Select } from '@blueprintjs/select'
+import InputValidationError from '@/components/validation/InputValidationError'
+
 const DataEntitiesSelector = (props) => {
   const {
     connections = [],
@@ -55,6 +57,8 @@ const DataEntitiesSelector = (props) => {
     onItemSelect = () => {},
     onRemove = () => {},
     onClear = () => {},
+    fieldHasError = () => {},
+    getFieldError = () => {},
     itemRenderer = (item, { handleClick, modifiers }) => (
       <MenuItem
         active={modifiers.active || selectedItems.find(i => i.id === item.id)}
@@ -101,7 +105,7 @@ const DataEntitiesSelector = (props) => {
             resetOnSelect={true}
             placeholder={placeholder}
             popoverProps={{ usePortal: false, minimal: true }}
-            className='multiselector-connections'
+            className='multiselector-entities'
             inline={true}
             fill={true}
             items={items}
diff --git a/config-ui/src/components/blueprints/create-workflow/DataScopes.jsx b/config-ui/src/components/blueprints/create-workflow/DataScopes.jsx
index a8d8fc06..24afeb6e 100644
--- a/config-ui/src/components/blueprints/create-workflow/DataScopes.jsx
+++ b/config-ui/src/components/blueprints/create-workflow/DataScopes.jsx
@@ -56,6 +56,8 @@ const DataScopes = (props) => {
     setProjects = () => {},
     setBoards = () => {},
     prevStep = () => {},
+    fieldHasError = () => {},
+    getFieldError = () => {},
     isSaving = false,
     isRunning = false,
   } = props
@@ -186,6 +188,8 @@ const DataScopes = (props) => {
                     // restrictedItems={getRestrictedDataEntities()}
                     onItemSelect={setDataEntities}
                     onClear={setDataEntities}
+                    fieldHasError={fieldHasError}
+                    getFieldError={getFieldError}
                     onRemove={setDataEntities}
                     disabled={isSaving}
                     configuredConnection={configuredConnection}
diff --git a/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx b/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx
index 05194e06..c4641b54 100644
--- a/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx
+++ b/config-ui/src/components/blueprints/create-workflow/DataTransformations.jsx
@@ -70,6 +70,8 @@ const DataTransformations = (props) => {
     onSave = () => {},
     onCancel = () => {},
     onClear = () => {},
+    fieldHasError = () => {},
+    getFieldError = () => {},
     isSaving = false,
     isSavingConnection = false,
     isRunning = false,
diff --git a/config-ui/src/components/pipelines/StageTaskName.jsx b/config-ui/src/components/pipelines/StageTaskName.jsx
index a5eb3c1f..80610392 100644
--- a/config-ui/src/components/pipelines/StageTaskName.jsx
+++ b/config-ui/src/components/pipelines/StageTaskName.jsx
@@ -53,15 +53,21 @@ const StageTaskName = (props) => {
       <Popover
         className='trigger-pipeline-activity-help'
         popoverClassName='popover-help-pipeline-activity'
-        // isOpen={showDetails && showDetails.ID === task.ID}
+        // isOpen={false}
         onClosed={onClose}
         position={Position.RIGHT}
         autoFocus={false}
         enforceFocus={false}
         usePortal={true}
-        disabled
+        // disabled
       >
-        <span className='task-plugin-text' ref={popoverTriggerRef} style={{ display: 'block', margin: '5px 0 5px 0' }}><strong>Task ID {task.id}</strong> {' '} {ProviderLabels[task?.plugin?.toUpperCase()]}</span>
+        <span className='task-plugin-text' ref={popoverTriggerRef} style={{ display: 'block', margin: '5px 0 5px 0' }}>
+          <strong>Task ID {task.id}</strong> {' '} {ProviderLabels[task?.plugin?.toUpperCase()]}{' '}
+          {task.plugin === Providers.GITHUB && task.plugin !== Providers.JENKINS && (<>@{task.options.owner}/{task.options.repo}</>)}
+          {task.plugin === Providers.JIRA && (<>Board ID {task.options.boardId}</>)}
+          {task.plugin === Providers.GITLAB && (<>Project ID {task.options.projectId}</>)}
+          {task.plugin === Providers.GITEXTRACTOR && (<>{task.options.repoId}</>)}
+        </span>
         <>
           <div style={{ textShadow: 'none', fontSize: '12px', padding: '12px', maxWidth: '400px' }}>
             <div style={{ display: 'flex', justifyContent: 'space-between' }}>
@@ -86,8 +92,9 @@ const StageTaskName = (props) => {
                   {task.plugin === Providers.GITEXTRACTOR && (<>{ProviderLabels.GITEXTRACTOR}</>)}
                   {task.plugin === Providers.FEISHU && (<>{ProviderLabels.FEISHU}</>)}
                   {task.plugin === Providers.JENKINS && (<>{ProviderLabels.JENKINS}</>)}
-                  {(task.plugin === Providers.GITLAB || task.plugin === Providers.JIRA) && (<>ID {task.options.projectId || task.options.boardId}</>)}
-                  {task.plugin === Providers.GITHUB && task.plugin !== Providers.JENKINS && (<>@{task.options.owner}/{task.options.repositoryName}</>)}
+                  {task.plugin === Providers.JIRA && (<>Board ID {task.options.boardId}</>)}
+                  {task.plugin === Providers.GITLAB && (<>Project ID {task.options.projectId}</>)}
+                  {task.plugin === Providers.GITHUB && task.plugin !== Providers.JENKINS && (<>@{task.options.owner}/{task.options.repo}</>)}
                 </H3>
                 {![Providers.JENKINS, Providers.REFDIFF, Providers.GITEXTRACTOR].includes(task.plugin) && (
                   <>{ProviderLabels[task.plugin?.toUpperCase()] || 'System Task'}<br /></>
@@ -107,15 +114,14 @@ const StageTaskName = (props) => {
                 {Number(task.status === 'TASK_COMPLETED' ? 100 : (task.progress / 1) * 100).toFixed(0)}%
               </div>
               <div style={{ padding: '0 0 10px 20px' }}>
-                {ProviderIcons[task.plugin.toLowerCase()](32, 32)}
+                {ProviderIcons[task.plugin?.toLowerCase()] ? ProviderIcons[task.plugin?.toLowerCase()](24, 24) : null}
               </div>
             </div>
             {task.status === 'TASK_CREATED' && (
               <div style={{ fontSize: '10px' }}>
-                <p style={{ fontSize: '14px' }}>
-                  This task (ID #{task.ID}) is <strong>PENDING</strong> and has not yet started.
+                <p style={{ fontSize: '12px' }}>
+                  Task #{task.id} is <strong>pending</strong> and has not yet started.
                 </p>
-                <strong>Created Date &mdash;</strong> <span>{dayjs(task.CreatedAt).format('L LT')}</span>
               </div>
             )}
             {task.status !== 'TASK_CREATED' && (
@@ -123,7 +129,7 @@ const StageTaskName = (props) => {
                 <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
                   <div>
                     <label style={{ color: Colors.GRAY2 }}>ID</label><br />
-                    <span>{task.ID}</span>
+                    <span>{task.id}</span>
                   </div>
                   <div style={{ marginLeft: '20px' }}>
                     <label style={{ color: Colors.GRAY2 }}>Created</label><br />
diff --git a/config-ui/src/data/NullBlueprint.js b/config-ui/src/data/NullBlueprint.js
index f40aa2b2..9f544c0d 100644
--- a/config-ui/src/data/NullBlueprint.js
+++ b/config-ui/src/data/NullBlueprint.js
@@ -46,7 +46,7 @@ const NullBlueprint = {
   cronConfig: '0 0 * * *',
   description: '',
   interval: 'daily',
-  enabled: BlueprintStatus.DISABLED,
+  enable: BlueprintStatus.DISABLED,
   mode: BlueprintMode.NORMAL,
   isManual: false
 }
diff --git a/config-ui/src/data/Task.js b/config-ui/src/data/Task.js
new file mode 100644
index 00000000..0564f743
--- /dev/null
+++ b/config-ui/src/data/Task.js
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+const StageStatus = {
+  PENDING: 'Pending',
+  COMPLETE: 'Complete',
+  FAILED: 'Failed',
+  ACTIVE: 'In Progress',
+}
+
+const TaskStatus = {
+  COMPLETE: 'TASK_COMPLETED',
+  FAILED: 'TASK_FAILED',
+  ACTIVE: 'TASK_RUNNING',
+  RUNNING: 'TASK_RUNNING',
+  CREATED: 'TASK_CREATED',
+  PENDING: 'TASK_CREATED',
+  CANCELLED: 'TASK_CANCELLED',
+}
+
+const TaskStatusLabels = {
+  [TaskStatus.COMPLETE]: 'Succeeded',
+  [TaskStatus.FAILED]: 'Failed',
+  [TaskStatus.ACTIVE]: 'In Progress',
+  [TaskStatus.RUNNING]: 'In Progress',
+  [TaskStatus.CREATED]: 'Created (Pending)',
+  [TaskStatus.PENDING]: 'Created (Pending)',
+  [TaskStatus.CANCELLED]: 'Cancelled',
+}
+
+const StatusColors = {
+  PENDING: '#292B3F',
+  COMPLETE: '#4DB764',
+  FAILED: '#E34040',
+  ACTIVE: '#7497F7',
+}
+
+const StatusBgColors = {
+  PENDING: 'transparent',
+  COMPLETE: '#EDFBF0',
+  FAILED: '#FEEFEF',
+  ACTIVE: '#F0F4FE',
+}
+
+export {
+  StageStatus,
+  TaskStatus,
+  TaskStatusLabels,
+  StatusColors,
+  StatusBgColors
+}
diff --git a/config-ui/src/data/TestBlueprintDetail.js b/config-ui/src/data/TestBlueprintDetail.js
new file mode 100644
index 00000000..451943fe
--- /dev/null
+++ b/config-ui/src/data/TestBlueprintDetail.js
@@ -0,0 +1,379 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+import React from 'react'
+import {
+  Intent,
+  Icon,
+  Colors,
+  Spinner
+} from '@blueprintjs/core'
+
+import { NullBlueprint } from '@/data/NullBlueprint'
+import { Providers, ProviderIcons } from '@/data/Providers'
+import { StageStatus, TaskStatus, TaskStatusLabels, StatusColors, StatusBgColors } from '@/data/Task'
+
+const EMPTY_RUN = {
+  id: null,
+  status: TaskStatus.CREATED,
+  statusLabel: TaskStatusLabels[TaskStatus.RUNNING],
+  icon: null,
+  startedAt: Date.now(),
+  duration: '0 min',
+  stage: 'Stage 1',
+  tasksTotal: 0,
+  tasksFinished: 0,
+  error: null,
+}
+
+const TEST_BLUEPRINT = {
+  ...NullBlueprint,
+  id: 1,
+  name: 'DevLake Daily Blueprint',
+  createdAt: new Date().toLocaleString(),
+  updatedAt: new Date().toLocaleString(),
+}
+
+const TEST_CONNECTIONS = [
+  {
+    id: 0,
+    provider: Providers.GITHUB,
+    name: 'Merico GitHub',
+    dataScope: 'merico-dev/ake, merico-dev/lake-website',
+    dataEntities: ['code', 'ticket', 'user'],
+  },
+  {
+    id: 0,
+    provider: Providers.JIRA,
+    name: 'Merico JIRA',
+    dataScope: 'Sprint Dev Board, DevLake Sync Board ',
+    dataEntities: ['ticket'],
+  },
+]
+
+// eslint-disable-next-line no-unused-vars
+const TEST_BLUEPRINT_API_RESPONSE = {
+  name: 'DEVLAKE (Hourly)',
+  mode: 'NORMAL',
+  plan: [
+    [
+      {
+        plugin: 'github',
+        subtasks: [
+          'collectApiRepo',
+          'extractApiRepo',
+          'collectApiIssues',
+          'extractApiIssues',
+          'collectApiPullRequests',
+          'extractApiPullRequests',
+          'collectApiComments',
+          'extractApiComments',
+          'collectApiEvents',
+          'extractApiEvents',
+          'collectApiPullRequestCommits',
+          'extractApiPullRequestCommits',
+          'collectApiPullRequestReviews',
+          'extractApiPullRequestReviewers',
+          'collectApiCommits',
+          'extractApiCommits',
+          'collectApiCommitStats',
+          'extractApiCommitStats',
+          'enrichPullRequestIssues',
+          'convertRepo',
+          'convertIssues',
+          'convertCommits',
+          'convertIssueLabels',
+          'convertPullRequestCommits',
+          'convertPullRequests',
+          'convertPullRequestLabels',
+          'convertPullRequestIssues',
+          'convertIssueComments',
+          'convertPullRequestComments',
+        ],
+        options: {
+          connectionId: 1,
+          owner: 'e2corporation',
+          repo: 'incubator-devlake',
+          transformationRules: {
+            issueComponent: '',
+            issuePriority: '',
+            issueSeverity: '',
+            issueTypeBug: '',
+            issueTypeIncident: '',
+            issueTypeRequirement: '',
+            prComponent: '',
+            prType: '',
+          },
+        },
+      },
+      {
+        plugin: 'gitextractor',
+        subtasks: null,
+        options: {
+          repoId: 'github:GithubRepo:1:506830252',
+          url: 'https://git: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@github.com/e2corporation/incubator-devlake.git',
+        },
+      },
+    ],
+    [
+      {
+        plugin: 'refdiff',
+        subtasks: null,
+        options: {
+          tagsLimit: 10,
+          tagsOrder: '',
+          tagsPattern: '',
+        },
+      },
+    ],
+  ],
+  enable: true,
+  cronConfig: '0 0 * * *',
+  isManual: false,
+  settings: {
+    version: '1.0.0',
+    connections: [
+      {
+        connectionId: 1,
+        plugin: 'github',
+        scope: [
+          {
+            entities: ['CODE', 'TICKET'],
+            options: {
+              owner: 'e2corporation',
+              repo: 'incubator-devlake',
+            },
+            transformation: {
+              prType: '',
+              prComponent: '',
+              issueSeverity: '',
+              issueComponent: '',
+              issuePriority: '',
+              issueTypeRequirement: '',
+              issueTypeBug: '',
+              issueTypeIncident: '',
+              refdiff: {
+                tagsOrder: '',
+                tagsPattern: '',
+                tagsLimit: 10,
+              },
+            },
+          },
+        ],
+      },
+    ],
+  },
+  id: 1,
+  createdAt: '2022-07-11T10:23:38.908-04:00',
+  updatedAt: '2022-07-11T10:23:38.908-04:00',
+}
+
+const TEST_STAGES = [
+  {
+    id: 1,
+    name: 'stage-1',
+    title: 'Stage 1',
+    status: StageStatus.COMPLETED,
+    icon: <Icon icon='tick-circle' size={14} color={StatusColors.COMPLETE} />,
+    tasks: [
+      {
+        id: 0,
+        provider: 'jira',
+        icon: ProviderIcons[Providers.JIRA](14, 14),
+        title: 'JIRA',
+        caption: 'STREAM Board',
+        duration: '4 min',
+        subTasksCompleted: 25,
+        recordsFinished: 1234,
+        message: 'All 25 subtasks completed',
+        status: TaskStatus.COMPLETE,
+      },
+      {
+        id: 0,
+        provider: 'jira',
+        icon: ProviderIcons[Providers.JIRA](14, 14),
+        title: 'JIRA',
+        caption: 'LAKE Board',
+        duration: '4 min',
+        subTasksCompleted: 25,
+        recordsFinished: 1234,
+        message: 'All 25 subtasks completed',
+        status: TaskStatus.COMPLETE,
+      },
+    ],
+    stageHeaderClassName: 'complete',
+  },
+  {
+    id: 2,
+    name: 'stage-2',
+    title: 'Stage 2',
+    status: StageStatus.PENDING,
+    icon: <Spinner size={14} intent={Intent.PRIMARY} />,
+    tasks: [
+      {
+        id: 0,
+        provider: 'jira',
+        icon: ProviderIcons[Providers.JIRA](14, 14),
+        title: 'JIRA',
+        caption: 'EE Board',
+        duration: '5 min',
+        subTasksCompleted: 25,
+        recordsFinished: 1234,
+        message: 'Subtask 5/25: Extracting Issues',
+        status: TaskStatus.ACTIVE,
+      },
+      {
+        id: 0,
+        provider: 'jira',
+        icon: ProviderIcons[Providers.JIRA](14, 14),
+        title: 'JIRA',
+        caption: 'EE Bugs Board',
+        duration: '0 min',
+        subTasksCompleted: 0,
+        recordsFinished: 0,
+        message: 'Invalid Board ID',
+        status: TaskStatus.FAILED,
+      },
+    ],
+    stageHeaderClassName: 'active',
+  },
+  {
+    id: 3,
+    name: 'stage-3',
+    title: 'Stage 3',
+    status: StageStatus.PENDING,
+    icon: null,
+    tasks: [
+      {
+        id: 0,
+        provider: 'github',
+        icon: ProviderIcons[Providers.GITHUB](14, 14),
+        title: 'GITHUB',
+        caption: 'merico-dev/lake',
+        duration: null,
+        subTasksCompleted: 0,
+        recordsFinished: 0,
+        message: 'Subtasks pending',
+        status: TaskStatus.CREATED,
+      },
+    ],
+    stageHeaderClassName: 'pending',
+  },
+  {
+    id: 4,
+    name: 'stage-4',
+    title: 'Stage 4',
+    status: StageStatus.PENDING,
+    icon: null,
+    tasks: [
+      {
+        id: 0,
+        providr: 'github',
+        icon: ProviderIcons[Providers.GITHUB](14, 14),
+        title: 'GITHUB',
+        caption: 'merico-dev/lake',
+        duration: null,
+        subTasksCompleted: 0,
+        recordsFinished: 0,
+        message: 'Subtasks pending',
+        status: TaskStatus.CREATED,
+      },
+    ],
+    stageHeaderClassName: 'pending',
+  },
+]
+
+const TEST_HISTORICAL_RUNS = [
+  {
+    id: 0,
+    status: 'TASK_COMPLETED',
+    statusLabel: 'Completed',
+    statusIcon: <Icon icon='tick-circle' size={14} color={Colors.GREEN5} />,
+    startedAt: '05/25/2022 0:00 AM',
+    completedAt: '05/25/2022 0:15 AM',
+    duration: '15 min',
+  },
+  {
+    id: 1,
+    status: 'TASK_COMPLETED',
+    statusLabel: 'Completed',
+    statusIcon: <Icon icon='tick-circle' size={14} color={Colors.GREEN5} />,
+    startedAt: '05/25/2022 0:00 AM',
+    completedAt: '05/25/2022 0:15 AM',
+    duration: '15 min',
+  },
+  {
+    id: 2,
+    status: 'TASK_FAILED',
+    statusLabel: 'Failed',
+    statusIcon: <Icon icon='delete' size={14} color={Colors.RED5} />,
+    startedAt: '05/25/2022 0:00 AM',
+    completedAt: '05/25/2022 0:00 AM',
+    duration: '0 min',
+  },
+  {
+    id: 3,
+    status: 'TASK_COMPLETED',
+    statusLabel: 'Completed',
+    statusIcon: <Icon icon='tick-circle' size={14} color={Colors.GREEN5} />,
+    startedAt: '05/25/2022 0:00 AM',
+    completedAt: '05/25/2022 0:15 AM',
+    duration: '15 min',
+  },
+  {
+    id: 4,
+    status: 'TASK_COMPLETED',
+    statusLabel: 'Completed',
+    statusIcon: <Icon icon='tick-circle' size={14} color={Colors.GREEN5} />,
+    startedAt: '05/25/2022 0:00 AM',
+    completedAt: '05/25/2022 0:15 AM',
+    duration: '15 min',
+  },
+  {
+    id: 5,
+    status: 'TASK_FAILED',
+    statusLabel: 'Failed',
+    statusIcon: <Icon icon='delete' size={14} color={Colors.RED5} />,
+    startedAt: '05/25/2022 0:00 AM',
+    completedAt: '05/25/2022 0:00 AM',
+    duration: '0 min',
+  },
+]
+
+const TEST_RUN = {
+  id: null,
+  status: TaskStatus.RUNNING,
+  statusLabel: TaskStatusLabels[TaskStatus.RUNNING],
+  icon: <Spinner size={18} intent={Intent.PRIMARY} />,
+  startedAt: '7/7/2022, 5:31:33 PM',
+  duration: '1 min',
+  stage: 'Stage 1',
+  tasksTotal: 5,
+  tasksFinished: 8,
+  // totalTasks: 13,
+  error: null,
+}
+
+export {
+  EMPTY_RUN,
+  TEST_RUN,
+  TEST_BLUEPRINT,
+  TEST_CONNECTIONS,
+  TEST_HISTORICAL_RUNS,
+  TEST_BLUEPRINT_API_RESPONSE,
+  TEST_STAGES
+}
diff --git a/config-ui/src/hooks/useBlueprintValidation.jsx b/config-ui/src/hooks/useBlueprintValidation.jsx
index 780dc0eb..003d23db 100644
--- a/config-ui/src/hooks/useBlueprintValidation.jsx
+++ b/config-ui/src/hooks/useBlueprintValidation.jsx
@@ -58,7 +58,7 @@ function useBlueprintValidation ({
   }, [])
 
   const validateRepositoryName = useCallback((set = []) => {
-    const repoRegExp = /([a-z0-9_-]){2,}\/([a-z0-9_-]){2,}/gi
+    const repoRegExp = /([a-z0-9_-]){2,}\/([a-z0-9_-]){2,}$/gi
     return set.every(i => i.match(repoRegExp))
   }, [])
 
@@ -68,7 +68,6 @@ function useBlueprintValidation ({
 
   const validate = useCallback(() => {
     const errs = []
-    // console.log('>> VALIDATING BLUEPRINT ', name)
 
     if (!name) {
       errs.push('Blueprint Name: Enter a valid Name')
@@ -164,7 +163,10 @@ function useBlueprintValidation ({
     projects,
     activeStep,
     activeProvider?.id,
-    activeConnection
+    activeConnection,
+    isValidCronExpression,
+    validateNumericSet,
+    validateRepositoryName
   ])
 
   const fieldHasError = useCallback((fieldId) => {
diff --git a/config-ui/src/hooks/usePipelineManager.jsx b/config-ui/src/hooks/usePipelineManager.jsx
index 8465966b..b2d7f8e0 100644
--- a/config-ui/src/hooks/usePipelineManager.jsx
+++ b/config-ui/src/hooks/usePipelineManager.jsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  *
  */
-import { useState, useEffect, useCallback } from 'react'
+import { useState, useEffect, useCallback, useMemo } from 'react'
 import { DEVLAKE_ENDPOINT } from '@/utils/config'
 import request from '@/utils/request'
 import { NullPipelineRun } from '@/data/NullPipelineRun'
@@ -56,6 +56,9 @@ function usePipelineManager (myPipelineName = `COLLECTION ${Date.now()}`, initia
     Providers.DBT
   ])
 
+  const PIPELINES_ENDPOINT = useMemo(() => `${DEVLAKE_ENDPOINT}/pipelines`, [DEVLAKE_ENDPOINT])
+  const [logfile, setLogfile] = useState('logging.tar.gz')
+
   const runPipeline = useCallback((runSettings = null) => {
     console.log('>> RUNNING PIPELINE....')
     try {
@@ -64,6 +67,7 @@ function usePipelineManager (myPipelineName = `COLLECTION ${Date.now()}`, initia
       ToastNotification.clear()
       console.log('>> DISPATCHING PIPELINE REQUEST', runSettings || settings)
       const run = async () => {
+        // @todo: remove "ID" fallback key when no longer needed
         const p = await request.post(`${DEVLAKE_ENDPOINT}/pipelines`, runSettings || settings)
         const t = await request.get(`${DEVLAKE_ENDPOINT}/pipelines/${p.data?.ID || p.data?.id}/tasks`)
         console.log('>> RAW PIPELINE DATA FROM API...', p.data)
@@ -122,12 +126,11 @@ function usePipelineManager (myPipelineName = `COLLECTION ${Date.now()}`, initia
         console.log('>> RAW PIPELINE TASKS DATA FROM API...', t.data)
         setActivePipeline({
           ...p.data,
-          ID: p.data.ID || p.data.id,
+          id: p.data.id,
           tasks: [...t.data.tasks]
         })
         setPipelineRun((pR) => refresh ? { ...p.data, ID: p.data.id, tasks: [...t.data.tasks] } : pR)
-        setLastRunId((lrId) => refresh ? p.data?.ID : lrId)
-        // ToastNotification.show({ message: `Fetched Pipeline ID - ${p.data?.ID}.`, intent: 'danger', icon: 'small-tick' })
+        setLastRunId((lrId) => refresh ? p.data?.id : lrId)
         setTimeout(() => {
           setIsFetching(false)
         }, 500)
@@ -167,10 +170,10 @@ function usePipelineManager (myPipelineName = `COLLECTION ${Date.now()}`, initia
         const p = await request.get(`${DEVLAKE_ENDPOINT}/pipelines${queryParams}`)
         console.log('>> RAW PIPELINES RUN DATA FROM API...', p.data?.pipelines)
         let pipelines = p.data && p.data.pipelines ? [...p.data.pipelines] : []
-        pipelines = pipelines.map(p => ({ ...p, ID: p.ID || p.id }))
+        // @todo: remove "ID" fallback key when no longer needed
+        pipelines = pipelines.map(p => ({ ...p, ID: p.id }))
         setPipelines(pipelines)
         setPipelineCount(p.data ? p.data.count : 0)
-        // ToastNotification.show({ message: `Fetched All Pipelines`, intent: 'danger', icon: 'small-tick' })
         setTimeout(() => {
           setIsFetchingAll(false)
         }, fetchTimeout)
@@ -211,6 +214,10 @@ function usePipelineManager (myPipelineName = `COLLECTION ${Date.now()}`, initia
 
   }, [pipelineName, initialTasks])
 
+  const getPipelineLogfile = useCallback((pipelineId = 0) => {
+    return `${PIPELINES_ENDPOINT}/${pipelineId}/${logfile}`
+  }, [PIPELINES_ENDPOINT, logfile])
+
   return {
     errors,
     isRunning,
@@ -226,6 +233,7 @@ function usePipelineManager (myPipelineName = `COLLECTION ${Date.now()}`, initia
     pipelines,
     pipelineCount,
     lastRunId,
+    logfile,
     runPipeline,
     cancelPipeline,
     fetchPipeline,
@@ -234,7 +242,8 @@ function usePipelineManager (myPipelineName = `COLLECTION ${Date.now()}`, initia
     buildPipelineStages,
     detectPipelineProviders,
     allowedProviders,
-    setAllowedProviders
+    setAllowedProviders,
+    getPipelineLogfile
   }
 }
 
diff --git a/config-ui/src/pages/blueprints/blueprint-detail.jsx b/config-ui/src/pages/blueprints/blueprint-detail.jsx
index d5fe2abc..f485d5c4 100644
--- a/config-ui/src/pages/blueprints/blueprint-detail.jsx
+++ b/config-ui/src/pages/blueprints/blueprint-detail.jsx
@@ -21,6 +21,7 @@ import { useParams, useHistory } from 'react-router-dom'
 import { DEVLAKE_ENDPOINT } from '@/utils/config'
 import request from '@/utils/request'
 import dayjs from '@/utils/time'
+import { saveAs } from 'file-saver'
 import {
   Button,
   Elevation,
@@ -41,412 +42,18 @@ import {
 import { NullBlueprint } from '@/data/NullBlueprint'
 import { NullPipelineRun } from '@/data/NullPipelineRun'
 import { Providers, ProviderLabels, ProviderIcons } from '@/data/Providers'
-
-// import {
-//   WorkflowSteps,
-//   WorkflowAdvancedSteps,
-//   DEFAULT_DATA_ENTITIES,
-//   DEFAULT_BOARDS,
-// } from '@/data/BlueprintWorkflow'
+import { StageStatus, TaskStatus, TaskStatusLabels, StatusColors, StatusBgColors } from '@/data/Task'
 
 import Nav from '@/components/Nav'
 import Sidebar from '@/components/Sidebar'
 import Content from '@/components/Content'
-import TaskActivity from '@/components/pipelines/TaskActivity'
+// import TaskActivity from '@/components/pipelines/TaskActivity'
 import CodeInspector from '@/components/pipelines/CodeInspector'
 import StageLane from '@/components/pipelines/StageLane'
+import { ToastNotification } from '@/components/Toast'
 
 import useBlueprintManager from '@/hooks/useBlueprintManager'
 import usePipelineManager from '@/hooks/usePipelineManager'
-// import useConnectionManager from '@/hooks/useConnectionManager'
-// import { DataEntityTypes } from '@/data/DataEntities'
-
-const StageStatus = {
-  PENDING: 'Pending',
-  COMPLETE: 'Complete',
-  FAILED: 'Failed',
-  ACTIVE: 'In Progress',
-}
-
-const TaskStatus = {
-  COMPLETE: 'TASK_COMPLETED',
-  FAILED: 'TASK_FAILED',
-  ACTIVE: 'TASK_RUNNING',
-  RUNNING: 'TASK_RUNNING',
-  CREATED: 'TASK_CREATED',
-  PENDING: 'TASK_CREATED',
-  CANCELLED: 'TASK_CANCELLED',
-}
-
-const TaskStatusLabels = {
-  [TaskStatus.COMPLETE]: 'Succeeded',
-  [TaskStatus.FAILED]: 'Failed',
-  [TaskStatus.ACTIVE]: 'In Progress',
-  [TaskStatus.RUNNING]: 'In Progress',
-  [TaskStatus.CREATED]: 'Created (Pending)',
-  [TaskStatus.PENDING]: 'Created (Pending)',
-  [TaskStatus.CANCELLED]: 'Cancelled',
-}
-
-const StatusColors = {
-  PENDING: '#292B3F',
-  COMPLETE: '#4DB764',
-  FAILED: '#E34040',
-  ACTIVE: '#7497F7',
-}
-
-// eslint-disable-next-line no-unused-vars
-const StatusBgColors = {
-  PENDING: 'transparent',
-  COMPLETE: '#EDFBF0',
-  FAILED: '#FEEFEF',
-  ACTIVE: '#F0F4FE',
-}
-
-// eslint-disable-next-line no-unused-vars
-const TEST_BLUEPRINT = {
-  ...NullBlueprint,
-  id: 1,
-  name: 'DevLake Daily Blueprint',
-  createdAt: new Date().toLocaleString(),
-  updatedAt: new Date().toLocaleString(),
-}
-
-// eslint-disable-next-line no-unused-vars
-const TEST_CONNECTIONS = [
-  {
-    id: 0,
-    provider: Providers.GITHUB,
-    name: 'Merico GitHub',
-    dataScope: 'merico-dev/ake, merico-dev/lake-website',
-    dataEntities: ['code', 'ticket', 'user'],
-  },
-  {
-    id: 0,
-    provider: Providers.JIRA,
-    name: 'Merico JIRA',
-    dataScope: 'Sprint Dev Board, DevLake Sync Board ',
-    dataEntities: ['ticket'],
-  },
-]
-
-const TEST_RUN = {
-  id: null,
-  status: TaskStatus.RUNNING,
-  statusLabel: TaskStatusLabels[TaskStatus.RUNNING],
-  icon: <Spinner size={18} intent={Intent.PRIMARY} />,
-  startedAt: '7/7/2022, 5:31:33 PM',
-  duration: '1 min',
-  stage: 'Stage 1',
-  tasksTotal: 5,
-  tasksFinished: 8,
-  // totalTasks: 13,
-  error: null,
-}
-
-// eslint-disable-next-line no-unused-vars
-const EMPTY_RUN = {
-  id: null,
-  status: TaskStatus.CREATED,
-  statusLabel: TaskStatusLabels[TaskStatus.RUNNING],
-  icon: null,
-  startedAt: Date.now(),
-  duration: '0 min',
-  stage: 'Stage 1',
-  tasksTotal: 0,
-  tasksFinished: 0,
-  // totalTasks: 0,
-  error: null,
-}
-
-// eslint-disable-next-line no-unused-vars
-const TEST_BLUEPRINT_API_RESPONSE = {
-  name: 'DEVLAKE (Hourly)',
-  mode: 'NORMAL',
-  plan: [
-    [
-      {
-        plugin: 'github',
-        subtasks: [
-          'collectApiRepo',
-          'extractApiRepo',
-          'collectApiIssues',
-          'extractApiIssues',
-          'collectApiPullRequests',
-          'extractApiPullRequests',
-          'collectApiComments',
-          'extractApiComments',
-          'collectApiEvents',
-          'extractApiEvents',
-          'collectApiPullRequestCommits',
-          'extractApiPullRequestCommits',
-          'collectApiPullRequestReviews',
-          'extractApiPullRequestReviewers',
-          'collectApiCommits',
-          'extractApiCommits',
-          'collectApiCommitStats',
-          'extractApiCommitStats',
-          'enrichPullRequestIssues',
-          'convertRepo',
-          'convertIssues',
-          'convertCommits',
-          'convertIssueLabels',
-          'convertPullRequestCommits',
-          'convertPullRequests',
-          'convertPullRequestLabels',
-          'convertPullRequestIssues',
-          'convertIssueComments',
-          'convertPullRequestComments',
-        ],
-        options: {
-          connectionId: 1,
-          owner: 'e2corporation',
-          repo: 'incubator-devlake',
-          transformationRules: {
-            issueComponent: '',
-            issuePriority: '',
-            issueSeverity: '',
-            issueTypeBug: '',
-            issueTypeIncident: '',
-            issueTypeRequirement: '',
-            prComponent: '',
-            prType: '',
-          },
-        },
-      },
-      {
-        plugin: 'gitextractor',
-        subtasks: null,
-        options: {
-          repoId: 'github:GithubRepo:1:506830252',
-          url: 'https://git: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@github.com/e2corporation/incubator-devlake.git',
-        },
-      },
-    ],
-    [
-      {
-        plugin: 'refdiff',
-        subtasks: null,
-        options: {
-          tagsLimit: 10,
-          tagsOrder: '',
-          tagsPattern: '',
-        },
-      },
-    ],
-  ],
-  enable: true,
-  cronConfig: '0 0 * * *',
-  isManual: false,
-  settings: {
-    version: '1.0.0',
-    connections: [
-      {
-        connectionId: 1,
-        plugin: 'github',
-        scope: [
-          {
-            entities: ['CODE', 'TICKET'],
-            options: {
-              owner: 'e2corporation',
-              repo: 'incubator-devlake',
-            },
-            transformation: {
-              prType: '',
-              prComponent: '',
-              issueSeverity: '',
-              issueComponent: '',
-              issuePriority: '',
-              issueTypeRequirement: '',
-              issueTypeBug: '',
-              issueTypeIncident: '',
-              refdiff: {
-                tagsOrder: '',
-                tagsPattern: '',
-                tagsLimit: 10,
-              },
-            },
-          },
-        ],
-      },
-    ],
-  },
-  id: 1,
-  createdAt: '2022-07-11T10:23:38.908-04:00',
-  updatedAt: '2022-07-11T10:23:38.908-04:00',
-}
-
-const TEST_STAGES = [
-  {
-    id: 1,
-    name: 'stage-1',
-    title: 'Stage 1',
-    status: StageStatus.COMPLETED,
-    icon: <Icon icon='tick-circle' size={14} color={StatusColors.COMPLETE} />,
-    tasks: [
-      {
-        id: 0,
-        provider: 'jira',
-        icon: ProviderIcons[Providers.JIRA](14, 14),
-        title: 'JIRA',
-        caption: 'STREAM Board',
-        duration: '4 min',
-        subTasksCompleted: 25,
-        recordsFinished: 1234,
-        message: 'All 25 subtasks completed',
-        status: TaskStatus.COMPLETE,
-      },
-      {
-        id: 0,
-        provider: 'jira',
-        icon: ProviderIcons[Providers.JIRA](14, 14),
-        title: 'JIRA',
-        caption: 'LAKE Board',
-        duration: '4 min',
-        subTasksCompleted: 25,
-        recordsFinished: 1234,
-        message: 'All 25 subtasks completed',
-        status: TaskStatus.COMPLETE,
-      },
-    ],
-    stageHeaderClassName: 'complete',
-  },
-  {
-    id: 2,
-    name: 'stage-2',
-    title: 'Stage 2',
-    status: StageStatus.PENDING,
-    icon: <Spinner size={14} intent={Intent.PRIMARY} />,
-    tasks: [
-      {
-        id: 0,
-        provider: 'jira',
-        icon: ProviderIcons[Providers.JIRA](14, 14),
-        title: 'JIRA',
-        caption: 'EE Board',
-        duration: '5 min',
-        subTasksCompleted: 25,
-        recordsFinished: 1234,
-        message: 'Subtask 5/25: Extracting Issues',
-        status: TaskStatus.ACTIVE,
-      },
-      {
-        id: 0,
-        provider: 'jira',
-        icon: ProviderIcons[Providers.JIRA](14, 14),
-        title: 'JIRA',
-        caption: 'EE Bugs Board',
-        duration: '0 min',
-        subTasksCompleted: 0,
-        recordsFinished: 0,
-        message: 'Invalid Board ID',
-        status: TaskStatus.FAILED,
-      },
-    ],
-    stageHeaderClassName: 'active',
-  },
-  {
-    id: 3,
-    name: 'stage-3',
-    title: 'Stage 3',
-    status: StageStatus.PENDING,
-    icon: null,
-    tasks: [
-      {
-        id: 0,
-        provider: 'github',
-        icon: ProviderIcons[Providers.GITHUB](14, 14),
-        title: 'GITHUB',
-        caption: 'merico-dev/lake',
-        duration: null,
-        subTasksCompleted: 0,
-        recordsFinished: 0,
-        message: 'Subtasks pending',
-        status: TaskStatus.CREATED,
-      },
-    ],
-    stageHeaderClassName: 'pending',
-  },
-  {
-    id: 4,
-    name: 'stage-4',
-    title: 'Stage 4',
-    status: StageStatus.PENDING,
-    icon: null,
-    tasks: [
-      {
-        id: 0,
-        providr: 'github',
-        icon: ProviderIcons[Providers.GITHUB](14, 14),
-        title: 'GITHUB',
-        caption: 'merico-dev/lake',
-        duration: null,
-        subTasksCompleted: 0,
-        recordsFinished: 0,
-        message: 'Subtasks pending',
-        status: TaskStatus.CREATED,
-      },
-    ],
-    stageHeaderClassName: 'pending',
-  },
-]
-
-const TEST_HISTORICAL_RUNS = [
-  {
-    id: 0,
-    status: 'TASK_COMPLETED',
-    statusLabel: 'Completed',
-    statusIcon: <Icon icon='tick-circle' size={14} color={Colors.GREEN5} />,
-    startedAt: '05/25/2022 0:00 AM',
-    completedAt: '05/25/2022 0:15 AM',
-    duration: '15 min',
-  },
-  {
-    id: 1,
-    status: 'TASK_COMPLETED',
-    statusLabel: 'Completed',
-    statusIcon: <Icon icon='tick-circle' size={14} color={Colors.GREEN5} />,
-    startedAt: '05/25/2022 0:00 AM',
-    completedAt: '05/25/2022 0:15 AM',
-    duration: '15 min',
-  },
-  {
-    id: 2,
-    status: 'TASK_FAILED',
-    statusLabel: 'Failed',
-    statusIcon: <Icon icon='delete' size={14} color={Colors.RED5} />,
-    startedAt: '05/25/2022 0:00 AM',
-    completedAt: '05/25/2022 0:00 AM',
-    duration: '0 min',
-  },
-  {
-    id: 3,
-    status: 'TASK_COMPLETED',
-    statusLabel: 'Completed',
-    statusIcon: <Icon icon='tick-circle' size={14} color={Colors.GREEN5} />,
-    startedAt: '05/25/2022 0:00 AM',
-    completedAt: '05/25/2022 0:15 AM',
-    duration: '15 min',
-  },
-  {
-    id: 4,
-    status: 'TASK_COMPLETED',
-    statusLabel: 'Completed',
-    statusIcon: <Icon icon='tick-circle' size={14} color={Colors.GREEN5} />,
-    startedAt: '05/25/2022 0:00 AM',
-    completedAt: '05/25/2022 0:15 AM',
-    duration: '15 min',
-  },
-  {
-    id: 5,
-    status: 'TASK_FAILED',
-    statusLabel: 'Failed',
-    statusIcon: <Icon icon='delete' size={14} color={Colors.RED5} />,
-    startedAt: '05/25/2022 0:00 AM',
-    completedAt: '05/25/2022 0:00 AM',
-    duration: '0 min',
-  },
-]
 
 const BlueprintDetail = (props) => {
   // eslint-disable-next-line no-unused-vars
@@ -454,8 +61,7 @@ const BlueprintDetail = (props) => {
   const { bId } = useParams()
 
   const [blueprintId, setBlueprintId] = useState()
-  // @todo: replace with live $blueprint from Hook
-  const [activeBlueprint, setActiveBlueprint] = useState(TEST_RUN)
+  const [activeBlueprint, setActiveBlueprint] = useState(NullBlueprint)
   // eslint-disable-next-line no-unused-vars
   const [blueprintConnections, setBlueprintConnections] = useState([])
   const [blueprintPipelines, setBlueprintPipelines] = useState([])
@@ -465,13 +71,14 @@ const BlueprintDetail = (props) => {
   const [showCurrentRunTasks, setShowCurrentRunTasks] = useState(true)
   const [showInspector, setShowInspector] = useState(false)
   const [currentStages, setCurrentStages] = useState([])
-  const [historicalRuns, setHistoricalRuns] = useState(TEST_HISTORICAL_RUNS)
+  const [historicalRuns, setHistoricalRuns] = useState([])
 
   const pollTimer = 5000
   const pollInterval = useRef()
   const [autoRefresh, setAutoRefresh] = useState(false)
 
   const [expandRun, setExpandRun] = useState(null)
+  const [isDownloading, setIsDownloading] = useState(false)
 
   const {
     // eslint-disable-next-line no-unused-vars
@@ -523,6 +130,8 @@ const BlueprintDetail = (props) => {
     allowedProviders,
     // eslint-disable-next-line no-unused-vars
     detectPipelineProviders,
+    logfile: pipelineLogFilename,
+    getPipelineLogfile
   } = usePipelineManager()
 
   const buildPipelineStages = useCallback((tasks = []) => {
@@ -597,12 +206,40 @@ const BlueprintDetail = (props) => {
         icon = <Icon icon='delete' size={14} color={Colors.RED5} />
         break
       case TaskStatus.CANCELLED:
+        icon = <Icon icon='undo' size={14} color={Colors.RED5} />
+        break
       case TaskStatus.CREATED:
+        icon = <Icon icon='stopwatch' size={14} color={Colors.GRAY3} />
         break
     }
     return icon
   }
 
+  const downloadPipelineLog = useCallback((pipeline) => {
+    console.log(`>>> DOWNLOADING PIPELINE #${pipeline?.id}  LOG...`, getPipelineLogfile(pipeline?.id))
+    setIsDownloading(true)
+    ToastNotification.clear()
+    let downloadStatus = 404
+    const checkDownloadStatus = async (pipeline) => {
+      const d = await request.get(getPipelineLogfile(pipeline?.id))
+      downloadStatus = d?.status
+    }
+    checkDownloadStatus()
+    if (pipeline?.id && downloadStatus === 200) {
+      saveAs(
+        getPipelineLogfile(pipeline?.id),
+        pipelineLogFilename
+      )
+      setIsDownloading(false)
+    } else if (pipeline?.id && downloadStatus === 404) {
+      ToastNotification.show({ message: 'Logfile not available', intent: 'danger', icon: 'error' })
+      setIsDownloading(false)
+    } else {
+      ToastNotification.show({ message: 'Pipeline Invalid or Missing', intent: 'danger', icon: 'error' })
+      setIsDownloading(false)
+    }
+  }, [getPipelineLogfile, pipelineLogFilename])
+
   useEffect(() => {
     setBlueprintId(bId)
     console.log('>>> REQUESTED BLUEPRINT ID ===', bId)
@@ -610,7 +247,6 @@ const BlueprintDetail = (props) => {
 
   useEffect(() => {
     if (blueprintId) {
-      // @todo: enable blueprint data fetch
       fetchBlueprint(blueprintId)
       fetchAllPipelines()
     }
@@ -648,7 +284,6 @@ const BlueprintDetail = (props) => {
 
   useEffect(() => {
     console.log('>>>> FETCHED ALL PIPELINES..', pipelines, activeBlueprint?.id)
-    //  {id: 5, status: 'TASK_FAILED', statusLabel: 'Failed', statusIcon: <Icon icon='delete' size={14} color={Colors.RED5} />,startedAt: '05/25/2022 0:00 AM', completedAt: '05/25/2022 0:00 AM', duration: '0 min' },
     setBlueprintPipelines(
       pipelines.filter((p) => p.blueprintId === activeBlueprint?.id)
     )
@@ -657,7 +292,6 @@ const BlueprintDetail = (props) => {
   useEffect(() => {
     console.log('>>>> RELATED BLUEPRINT PIPELINES..', blueprintPipelines)
     setLastPipeline(blueprintPipelines[0])
-    // blueprintPipelines.filter(p => p.status !== TaskStatus.RUNNING).map
     setHistoricalRuns(
       blueprintPipelines.map((p, pIdx) => ({
         id: p.id,
@@ -694,7 +328,6 @@ const BlueprintDetail = (props) => {
         stage: `Stage ${lastPipeline.stage}`,
         tasksFinished: Number(lastPipeline.finishedTasks),
         tasksTotal: Number(lastPipeline.totalTasks),
-        // totalTasks: Number(lastPipeline.totalTasks),
         error: lastPipeline.message || null,
       }))
     }
@@ -834,32 +467,6 @@ const BlueprintDetail = (props) => {
               </div>
             </div>
 
-            {/* <div className='blueprint-connections' style={{ width: '100%', alignSelf: 'flex-start' }}>
-              <h3>Overview</h3>
-              <Card elevation={Elevation.TWO} style={{ padding: '2px' }}>
-              <table className='bp3-html-table bp3-html-table-bordered connections-overview-table' style={{ width: '100%' }}>
-                <thead>
-                  <tr>
-                    <th style={{ minWidth: '200px' }}>Data Connection</th>
-                    <th style={{ width: '100%' }}>Data Scope</th>
-                  </tr>
-                </thead>
-                <tbody>
-                  {blueprintConnections?.map((c, cIdx) => (
-                  <tr key={`connection-row-key-${cIdx}`}>
-                    <td>
-                      {c.name}
-                    </td>
-                    <td>
-                      {c.dataScope}{' '}
-                    </td>
-                  </tr>
-                  ))}
-                </tbody>
-              </table>
-              </Card>
-            </div> */}
-
             <div
               className='blueprint-run'
               style={{
@@ -1016,7 +623,6 @@ const BlueprintDetail = (props) => {
                   >
                     <div
                       className='pipeline-task-activity' style={{
-                      // padding: '20px',
                         flex: 1,
                         padding: Object.keys(currentStages).length === 1 ? '0' : 0,
                         overflow: 'hidden',
@@ -1033,96 +639,7 @@ const BlueprintDetail = (props) => {
                         </div>
                       )}
                     </div>
-                    {/* {currentStages.map((stage, stageIdx) => (
-                      <div
-                        className='run-stage'
-                        key={`run-stage-key-${stageIdx}`}
-                        style={{ flex: 1, margin: '0 4px' }}
-                      >
-                        <h3
-                          className={`stage-header ${stage?.stageHeaderClassName}`}
-                          style={{ margin: '0', padding: '7px' }}
-                        >
-                          <span style={{ float: 'right' }}>{stage?.icon}</span>
-                          {stage?.title}
-                        </h3>
-                        {showCurrentRunTasks && (
-                          <div className='task-activity'>
-                            {stage.tasks.map((stageTask, stIdx) => (
-                              <div
-                                className='stage-task'
-                                key={`stage-task-key-${stIdx}`}
-                                style={{
-                                  display: 'flex',
-                                  flexDirection: 'column',
-                                }}
-                              >
-                                <div
-                                  className='stage-task-info'
-                                  style={{ display: 'flex', padding: '8px' }}
-                                >
-                                  <div
-                                    className='task-icon'
-                                    style={{ minWidth: '24px' }}
-                                  >
-                                    {stageTask.icon}
-                                  </div>
-                                  <div
-                                    className='task-title'
-                                    style={{ flex: 1 }}
-                                  >
-                                    <div style={{ marginBottom: '8px' }}>
-                                      <strong>{stageTask.title}</strong>{' '}
-                                      {stageTask?.caption}
-                                    </div>
-                                    <div
-                                      className='stage-task-progress'
-                                      style={{
-                                        color:
-                                          stageTask?.status ===
-                                          TaskStatus.FAILED
-                                            ? StatusColors.FAILED
-                                            : 'inherit',
-                                      }}
-                                    >
-                                      <div>{stageTask?.message}</div>
-                                      <div>
-                                        {stageTask?.recordsFinished} records
-                                        finished
-                                      </div>
-                                    </div>
-                                  </div>
-                                  <div
-                                    className='task-duration'
-                                    style={{
-                                      display: 'flex',
-                                      justifyContent: 'center',
-                                      alignItems: 'center',
-                                      color: StatusColors[stageTask?.status],
-                                    }}
-                                  >
-                                    {stageTask.duration}{' '}
-                                    {stageTask?.status ===
-                                      TaskStatus.FAILED && (
-                                      <>
-                                        ({TaskStatusLabels[TaskStatus.FAILED]})
-                                      </>
-                                    )}
-                                    {stageTask?.status ===
-                                      TaskStatus.ACTIVE && (
-                                      <>
-                                        ({TaskStatusLabels[TaskStatus.ACTIVE]})
-                                      </>
-                                    )}
-                                  </div>
-                                </div>
-                                <Divider />
-                              </div>
-                            ))}
-                          </div>
-                        )}
-                      </div>
-                    ))} */}
+
                     <Button
                       icon={
                         showCurrentRunTasks ? 'chevron-down' : 'chevron-right'
@@ -1242,20 +759,20 @@ const BlueprintDetail = (props) => {
                               onClick={() => inspectRun(blueprintPipelines.find(p => p.id === run.id))}
                             />
                           </Tooltip>
-                          {/* <Tooltip
+                          <Tooltip
                             intent={Intent.PRIMARY}
                             content='View Full Log'
-                          > */}
-                          <Button
-                            intent={Intent.NONE}
-                            minimal
-                            small
-                            icon='document'
-                            style={{ marginLeft: '10px' }}
-                            // @todo: enable log view dialog support feature
-                            disabled
-                          />
-                          {/* </Tooltip> */}
+                          >
+                            <Button
+                              intent={Intent.NONE}
+                              loading={isDownloading}
+                              minimal
+                              small
+                              icon='document'
+                              style={{ marginLeft: '10px' }}
+                              onClick={() => downloadPipelineLog(blueprintPipelines.find(p => p.id === run.id))}
+                            />
+                          </Tooltip>
                           <Tooltip
                             intent={Intent.PRIMARY}
                             content='Show Run Activity'
diff --git a/config-ui/src/pages/blueprints/create-blueprint.jsx b/config-ui/src/pages/blueprints/create-blueprint.jsx
index a8168e6e..df0aad25 100644
--- a/config-ui/src/pages/blueprints/create-blueprint.jsx
+++ b/config-ui/src/pages/blueprints/create-blueprint.jsx
@@ -1190,6 +1190,8 @@ const CreateBlueprint = (props) => {
                       onSave={handleTransformationSave}
                       onCancel={handleTransformationCancel}
                       onClear={handleTransformationClear}
+                      fieldHasError={fieldHasError}
+                      getFieldError={getFieldError}
                       jiraProxyError={jiraProxyError}
                       isFetchingJIRA={isFetchingJIRA}
                     />
diff --git a/config-ui/src/pages/blueprints/index.jsx b/config-ui/src/pages/blueprints/index.jsx
index bd40cdb8..5a669db2 100644
--- a/config-ui/src/pages/blueprints/index.jsx
+++ b/config-ui/src/pages/blueprints/index.jsx
@@ -249,6 +249,9 @@ const Blueprints = (props) => {
         case 'monthly':
           setFilteredBlueprints(blueprints.filter(b => b.cronConfig === getCronPreset(activeFilterStatus).cronConfig))
           break
+        case 'manual':
+          setFilteredBlueprints(blueprints.filter(b => b.isManual))
+          break
         case 'custom':
           setFilteredBlueprints(blueprints.filter(
             b => b.cronConfig !== getCronPreset('hourly').cronConfig &&
diff --git a/config-ui/src/pages/configure/settings/github.jsx b/config-ui/src/pages/configure/settings/github.jsx
index b60bf47a..40686121 100644
--- a/config-ui/src/pages/configure/settings/github.jsx
+++ b/config-ui/src/pages/configure/settings/github.jsx
@@ -306,7 +306,7 @@ export default function GithubSettings (props) {
         </>
       )}
 
-      {(entities?.length === 0 || entities.some(e => e.value === DataEntityTypes.CROSSDOMAIN)) && (
+      {(entities?.length === 0 || entities.every(e => e.value === DataEntityTypes.CROSSDOMAIN)) && (
         <div className='headlineContainer'>
           <h5>No Data Entities</h5>
           <p className='description'>