From fbf8c1466939e85f1c345853817697293f1a3204 Mon Sep 17 00:00:00 2001 From: Johnson Kwok <78053898+johnson-mage@users.noreply.github.com> Date: Wed, 15 Mar 2023 09:26:36 -0700 Subject: [PATCH] [jk] Add pipeline description (#2203) * [jk] Add description to pipeline model * [jk] Add description column * [jk] Edit description * [jk] Use textarea for description editing * [jk] Allow updating to empty description * [jk] Update unit tests * [jk] Fix bug --- mage_ai/api/presenters/PipelinePresenter.py | 1 + mage_ai/data_preparation/models/pipeline.py | 7 +++ mage_ai/frontend/interfaces/PipelineType.ts | 1 + .../oracle/elements/Inputs/InputModal.tsx | 12 ++++- .../oracle/elements/Inputs/TextArea.tsx | 16 +++--- mage_ai/frontend/pages/pipelines/index.tsx | 52 +++++++++++++++---- .../data_preparation/models/test_pipeline.py | 6 ++- 7 files changed, 73 insertions(+), 22 deletions(-) diff --git a/mage_ai/api/presenters/PipelinePresenter.py b/mage_ai/api/presenters/PipelinePresenter.py index 25ef9323dd00..a17e8dcd30ce 100644 --- a/mage_ai/api/presenters/PipelinePresenter.py +++ b/mage_ai/api/presenters/PipelinePresenter.py @@ -7,6 +7,7 @@ class PipelinePresenter(BasePresenter): default_attributes = [ 'blocks', 'data_integration', + 'description', 'name', 'type', 'uuid', diff --git a/mage_ai/data_preparation/models/pipeline.py b/mage_ai/data_preparation/models/pipeline.py index b8c56d569514..58d46586e408 100644 --- a/mage_ai/data_preparation/models/pipeline.py +++ b/mage_ai/data_preparation/models/pipeline.py @@ -39,6 +39,7 @@ def __init__(self, uuid, repo_path=None, config=None, repo_config=None, catalog= self.block_configs = [] self.blocks_by_uuid = {} self.data_integration = None + self.description = None self.extensions = {} self.name = None self.repo_path = repo_path or get_repo_path() @@ -376,6 +377,7 @@ def load_config(self, config, catalog=None): else: self.data_integration = catalog self.name = config.get('name') + self.description = config.get('description') self.type = config.get('type') or self.type self.block_configs = config.get('blocks') or [] @@ -458,6 +460,7 @@ def __initialize_blocks_by_uuid( def to_dict_base(self, exclude_data_integration=False) -> Dict: base = dict( data_integration=self.data_integration if not exclude_data_integration else None, + description=self.description, name=self.name, type=self.type.value if type(self.type) is not str else self.type, uuid=self.uuid, @@ -619,6 +622,10 @@ async def update(self, data, update_content=False): should_save = False + if 'description' in data and data['description'] != self.description: + self.description = data['description'] + should_save = True + if 'type' in data and data['type'] != self.type: """ Update kernel diff --git a/mage_ai/frontend/interfaces/PipelineType.ts b/mage_ai/frontend/interfaces/PipelineType.ts index 3249b9b05b07..18690411675e 100644 --- a/mage_ai/frontend/interfaces/PipelineType.ts +++ b/mage_ai/frontend/interfaces/PipelineType.ts @@ -45,6 +45,7 @@ export default interface PipelineType { data_integration?: { catalog: CatalogType; }; + description?: string; extensions?: PipelineExtensionsType; id?: number; metadata?: PipelineMetadataType; diff --git a/mage_ai/frontend/oracle/elements/Inputs/InputModal.tsx b/mage_ai/frontend/oracle/elements/Inputs/InputModal.tsx index a1348ca9352c..f1b9137b03ab 100644 --- a/mage_ai/frontend/oracle/elements/Inputs/InputModal.tsx +++ b/mage_ai/frontend/oracle/elements/Inputs/InputModal.tsx @@ -6,6 +6,7 @@ import KeyboardShortcutButton from '@oracle/elements/Button/KeyboardShortcutButt import Panel from '@oracle/components/Panel'; import Spacing from '@oracle/elements/Spacing'; import Text from '@oracle/elements/Text'; +import TextArea from '@oracle/elements/Inputs/TextArea'; import TextInput from '@oracle/elements/Inputs/TextInput'; type InputModalProps = { @@ -13,8 +14,10 @@ type InputModalProps = { isLoading?: boolean; maxWidth?: number; minWidth?: number; + noEmptyValue?: boolean; onClose: () => void; onSave: (value: string) => void; + textArea?: boolean; title: string; value: string; }; @@ -24,13 +27,16 @@ function InputModal({ isLoading, maxWidth, minWidth, + noEmptyValue, onClose, onSave, + textArea, title, value, }: InputModalProps) { const refTextInput = useRef(null); const [inputValue, setInputValue] = useState(value); + const TextEl = textArea ? TextArea : TextInput; useEffect(() => { refTextInput?.current?.focus(); @@ -46,10 +52,12 @@ function InputModal({ - setInputValue(e.target.value)} ref={refTextInput} + // @ts-ignore + rows={textArea ? 7 : null} value={inputValue} /> @@ -69,7 +77,7 @@ function InputModal({ inline loading={isLoading} onClick={() => { - if (inputValue === value || !inputValue) { + if (inputValue === value || (noEmptyValue && !inputValue)) { onClose(); return; } diff --git a/mage_ai/frontend/oracle/elements/Inputs/TextArea.tsx b/mage_ai/frontend/oracle/elements/Inputs/TextArea.tsx index b1082e87d5b4..72194d180b78 100644 --- a/mage_ai/frontend/oracle/elements/Inputs/TextArea.tsx +++ b/mage_ai/frontend/oracle/elements/Inputs/TextArea.tsx @@ -11,14 +11,12 @@ const TextInputStyle = styled.textarea` ${SHARED_INPUT_STYLES} `; -const TextInput = ({ rows = 3, ...props }: TextAreaProps, ref) => { - return ( - } - ref={ref} - /> - ); -}; +const TextInput = ({ rows = 3, ...props }: TextAreaProps, ref) => ( + } + ref={ref} + /> +); export default React.forwardRef(TextInput); diff --git a/mage_ai/frontend/pages/pipelines/index.tsx b/mage_ai/frontend/pages/pipelines/index.tsx index 7325e772dbba..09b9b4f2f7be 100644 --- a/mage_ai/frontend/pages/pipelines/index.tsx +++ b/mage_ai/frontend/pages/pipelines/index.tsx @@ -106,6 +106,7 @@ function PipelineListPage() { })); fetchPipelines(); hideInputModal?.(); + setSelectedPipeline(null); }, onErrorCallback: (response, errors) => { const pipelineUUID = response?.url_parameters?.pk; @@ -140,28 +141,42 @@ function PipelineListPage() { ); const [showInputModal, hideInputModal] = useModal(({ + pipelineDescription, pipelineName, }: { - pipelineName: string; + pipelineDescription?: string; + pipelineName?: string; }) => ( { + onSave={(value: string) => { if (selectedPipeline) { const selectedPipelineUUID = selectedPipeline.uuid; + const pipelineUpdateRequestBody: PipelineType = { + uuid: selectedPipelineUUID, + }; + if (pipelineName) { + pipelineUpdateRequestBody.name = value; + } else { + pipelineUpdateRequestBody.description = value; + } + setPipelinesEditing(prev => ({ ...prev, [selectedPipelineUUID]: true, })); - updatePipeline({ - name, - uuid: selectedPipelineUUID, - }); + updatePipeline(pipelineUpdateRequestBody); } }} - title="Rename pipeline" - value={pipelineName} + textArea={!pipelineName} + title={pipelineName + ? 'Rename pipeline' + : 'Edit description' + } + value={pipelineName ? pipelineName : pipelineDescription} /> ), {} , [ isLoadingUpdate, @@ -228,7 +243,12 @@ function PipelineListPage() { { label: () => 'Rename pipeline', onClick: () => showInputModal({ pipelineName: selectedPipeline?.name }), - uuid: 'Pipelines/MoreActionsMenu/rename', + uuid: 'Pipelines/MoreActionsMenu/Rename', + }, + { + label: () => 'Edit description', + onClick: () => showInputModal({ pipelineDescription: selectedPipeline?.description }), + uuid: 'Pipelines/MoreActionsMenu/EditDescription', }, ]} query={query} @@ -266,7 +286,7 @@ function PipelineListPage() { ): ( '', @@ -278,6 +298,9 @@ function PipelineListPage() { { uuid: 'Name', }, + { + uuid: 'Description', + }, { uuid: 'Type', }, @@ -307,6 +330,7 @@ function PipelineListPage() { rows={pipelines.map((pipeline, idx) => { const { blocks, + description, name, schedules, type, @@ -368,6 +392,14 @@ function PipelineListPage() { {uuid} , + + {description} + , diff --git a/mage_ai/tests/data_preparation/models/test_pipeline.py b/mage_ai/tests/data_preparation/models/test_pipeline.py index 70b625c68b7d..8bde84d629be 100644 --- a/mage_ai/tests/data_preparation/models/test_pipeline.py +++ b/mage_ai/tests/data_preparation/models/test_pipeline.py @@ -29,6 +29,7 @@ def test_add_block(self): self.assertEqual(pipeline.to_dict(), dict( data_integration=None, + description=None, name='test pipeline 2', uuid='test_pipeline_2', type='python', @@ -153,6 +154,7 @@ def test_delete_block(self): pipeline = Pipeline('test_pipeline_3', self.repo_path) self.assertEqual(pipeline.to_dict(), dict( data_integration=None, + description=None, name='test pipeline 3', uuid='test_pipeline_3', type='python', @@ -222,6 +224,7 @@ def test_execute(self): pipeline.execute_sync() self.assertEqual(pipeline.to_dict(), dict( data_integration=None, + description=None, name='test pipeline 4', uuid='test_pipeline_4', type='python', @@ -312,6 +315,7 @@ def test_execute_multiple_paths(self): pipeline.execute_sync() self.assertEqual(pipeline.to_dict(), dict( data_integration=None, + description=None, name='test pipeline 5', uuid='test_pipeline_5', type='python', @@ -523,11 +527,11 @@ def test_save_and_get_data_integration_catalog(self): self.assertTrue(os.path.exists(pipeline.config_path)) with open(pipeline.config_path) as f: config_json = yaml.full_load(f) - print('WTFFFFFFFFFFF', config_json) self.assertEqual( config_json, { "data_integration": None, + "description": None, "extensions": {}, "name": "test_pipeline_9", "type": "integration",