From 1646867dc8ee6ec4501de626ef11630cdbcaa0cb Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Tue, 21 Feb 2023 13:51:35 -0800 Subject: [PATCH 01/25] Filter available runtimes based on configuration setting Signed-off-by: Kevin Bates --- elyra/elyra_app.py | 9 +++++- elyra/pipeline/processor.py | 42 +++++++++++++++++++++----- elyra/tests/pipeline/test_processor.py | 29 ++++++++++++++++++ 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/elyra/elyra_app.py b/elyra/elyra_app.py index c3d3a924c..c4f494c4d 100644 --- a/elyra/elyra_app.py +++ b/elyra/elyra_app.py @@ -60,7 +60,14 @@ class ElyraApp(ExtensionAppJinjaMixin, ExtensionApp): extension_url = "/lab" load_other_extensions = True - classes = [FileMetadataCache, MetadataManager, PipelineProcessor, ComponentCatalogConnector, ComponentCache] + classes = [ + FileMetadataCache, + MetadataManager, + PipelineProcessorRegistry, + PipelineProcessor, + ComponentCatalogConnector, + ComponentCache, + ] # Local path to static files directory. static_paths = [ diff --git a/elyra/pipeline/processor.py b/elyra/pipeline/processor.py index ea20e491a..a27511bc1 100644 --- a/elyra/pipeline/processor.py +++ b/elyra/pipeline/processor.py @@ -32,6 +32,8 @@ import entrypoints from minio.error import S3Error from traitlets.config import Bool +from traitlets.config import default +from traitlets.config import List as ListTrait from traitlets.config import LoggingConfigurable from traitlets.config import SingletonConfigurable from traitlets.config import Unicode @@ -61,10 +63,27 @@ class PipelineProcessorRegistry(SingletonConfigurable): - _processors: Dict[str, PipelineProcessor] = {} + _processors: Dict[str, PipelineProcessor] + + # Runtimes + runtimes_env = "ELYRA_PROCESSOR_RUNTIMES" + runtimes = ListTrait( + default_value=None, + config=True, + allow_none=True, + help="""The runtimes to use during this Elyra instance. (env ELYRA_PROCESSOR_RUNTIMES)""", + ) + + @default("runtimes") + def _runtimes_default(self) -> Optional[List[str]]: + env_value = os.getenv(self.runtimes_env) + if env_value: + return env_value.replace(" ", "").split(",") + return None def __init__(self, **kwargs): root_dir: Optional[str] = kwargs.pop("root_dir", None) + self._processors = {} super().__init__(**kwargs) self.root_dir = get_expanded_path(root_dir) # Register all known processors based on entrypoint configuration @@ -72,10 +91,14 @@ def __init__(self, **kwargs): try: # instantiate an actual instance of the processor processor_instance = processor.load()(root_dir=self.root_dir, parent=kwargs.get("parent")) - self.log.info( - f"Registering {processor.name} processor '{processor.module_name}.{processor.object_name}'..." - ) - self.add_processor(processor_instance) + if not self.runtimes or processor.name in self.runtimes: + self._add_processor(processor_instance) + else: + self.log.info( + f"Although runtime '{processor.name}' is installed, it is not in the set of " + f"runtimes configured via '--PipelineProcessorRegistry.runtimes' and will not " + f"be available." + ) except Exception as err: # log and ignore initialization errors self.log.error( @@ -83,9 +106,12 @@ def __init__(self, **kwargs): f'"{processor.module_name}.{processor.object_name}" - {err}' ) - def add_processor(self, processor): - self.log.debug(f"Registering {processor.type.value} runtime processor '{processor.name}'") - self._processors[processor.name] = processor + def _add_processor(self, processor_instance): + self.log.info( + f"Registering {processor_instance.name} processor " + f"'{processor_instance.__class__.__module__}.{processor_instance.__class__.__name__}'..." + ) + self._processors[processor_instance.name] = processor_instance def get_processor(self, processor_name: str): if self.is_valid_processor(processor_name): diff --git a/elyra/tests/pipeline/test_processor.py b/elyra/tests/pipeline/test_processor.py index 00121b61e..80f593489 100644 --- a/elyra/tests/pipeline/test_processor.py +++ b/elyra/tests/pipeline/test_processor.py @@ -20,6 +20,7 @@ from elyra.pipeline.kfp.processor_kfp import KfpPipelineProcessor from elyra.pipeline.pipeline import GenericOperation +from elyra.pipeline.processor import PipelineProcessorRegistry from elyra.pipeline.properties import ElyraProperty @@ -280,3 +281,31 @@ def test_process_dictionary_value_function(processor: KfpPipelineProcessor): dict_as_str = "{'key1': {key2: 2}, 'key3': ['elem1', 'elem2']}" assert processor._process_dictionary_value(dict_as_str) == dict_as_str + + +@pytest.fixture +def processor_registry(monkeypatch, expected_runtimes) -> PipelineProcessorRegistry: + if expected_runtimes: + monkeypatch.setenv("ELYRA_PROCESSOR_RUNTIMES", ",".join(expected_runtimes)) + ppr = PipelineProcessorRegistry.instance() + yield ppr + PipelineProcessorRegistry.clear_instance() + + +@pytest.mark.parametrize( + "expected_runtimes", + [ + ["local"], + ["airflow", "kfp"], + ["local", "airflow"], + None, + ], +) +def test_processor_registry_filtering(expected_runtimes, processor_registry): + + if expected_runtimes is None: + expected_runtimes = ["local", "kfp", "airflow"] + runtimes = processor_registry.get_all_processors() + assert len(runtimes) == len(expected_runtimes) + for runtime in runtimes: + assert runtime.name in expected_runtimes From e6eb6bfbed34258d056d0aca1da263b683eea9e9 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Tue, 21 Feb 2023 14:20:43 -0800 Subject: [PATCH 02/25] Fix lint issue Signed-off-by: Kevin Bates --- elyra/tests/pipeline/test_processor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/elyra/tests/pipeline/test_processor.py b/elyra/tests/pipeline/test_processor.py index 80f593489..dc1b1220d 100644 --- a/elyra/tests/pipeline/test_processor.py +++ b/elyra/tests/pipeline/test_processor.py @@ -302,7 +302,6 @@ def processor_registry(monkeypatch, expected_runtimes) -> PipelineProcessorRegis ], ) def test_processor_registry_filtering(expected_runtimes, processor_registry): - if expected_runtimes is None: expected_runtimes = ["local", "kfp", "airflow"] runtimes = processor_registry.get_all_processors() From c58cf9759571eb7e41d03ee5aef7f671260605f6 Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Tue, 21 Feb 2023 14:39:42 -0800 Subject: [PATCH 03/25] Clear PipelineProcessorRegistry instance to prevent pre-test side-effects Signed-off-by: Kevin Bates --- elyra/tests/pipeline/test_processor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/elyra/tests/pipeline/test_processor.py b/elyra/tests/pipeline/test_processor.py index dc1b1220d..cf4aeff6a 100644 --- a/elyra/tests/pipeline/test_processor.py +++ b/elyra/tests/pipeline/test_processor.py @@ -285,6 +285,7 @@ def test_process_dictionary_value_function(processor: KfpPipelineProcessor): @pytest.fixture def processor_registry(monkeypatch, expected_runtimes) -> PipelineProcessorRegistry: + PipelineProcessorRegistry.clear_instance() if expected_runtimes: monkeypatch.setenv("ELYRA_PROCESSOR_RUNTIMES", ",".join(expected_runtimes)) ppr = PipelineProcessorRegistry.instance() From 88a84b2633da5ad977b64b84bfc64a40f5935f6f Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Tue, 21 Feb 2023 15:14:53 -0800 Subject: [PATCH 04/25] Attempt to address CodeQL issue Signed-off-by: Kevin Bates --- elyra/pipeline/processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elyra/pipeline/processor.py b/elyra/pipeline/processor.py index a27511bc1..018230a2c 100644 --- a/elyra/pipeline/processor.py +++ b/elyra/pipeline/processor.py @@ -82,10 +82,10 @@ def _runtimes_default(self) -> Optional[List[str]]: return None def __init__(self, **kwargs): - root_dir: Optional[str] = kwargs.pop("root_dir", None) - self._processors = {} super().__init__(**kwargs) + root_dir: Optional[str] = kwargs.pop("root_dir", None) self.root_dir = get_expanded_path(root_dir) + self._processors = {} # Register all known processors based on entrypoint configuration for processor in entrypoints.get_group_all("elyra.pipeline.processors"): try: From 18d39da58494c575f816ff2534461bff49c65033 Mon Sep 17 00:00:00 2001 From: Patrick Titzler Date: Wed, 22 Feb 2023 13:36:02 -0800 Subject: [PATCH 05/25] Add pipeline editor configuration topic to user guide Signed-off-by: Patrick Titzler --- ...editor-kfp-only-in-jupyterlab-launcher.png | Bin 0 -> 8485 bytes ...e-editor-subset-in-jupyterlab-launcher.png | Bin 0 -> 12571 bytes ...ne-editor-tiles-in-jupyterlab-launcher.png | Bin 0 -> 17500 bytes docs/source/index.rst | 1 + .../source/user_guide/jupyterlab-interface.md | 4 +- .../pipeline-editor-configuration.md | 91 ++++++++++++++++++ 6 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 docs/source/images/user_guide/pipeline-editor-configuration/pipeline-editor-kfp-only-in-jupyterlab-launcher.png create mode 100644 docs/source/images/user_guide/pipeline-editor-configuration/pipeline-editor-subset-in-jupyterlab-launcher.png create mode 100644 docs/source/images/user_guide/pipeline-editor-configuration/pipeline-editor-tiles-in-jupyterlab-launcher.png create mode 100644 docs/source/user_guide/pipeline-editor-configuration.md diff --git a/docs/source/images/user_guide/pipeline-editor-configuration/pipeline-editor-kfp-only-in-jupyterlab-launcher.png b/docs/source/images/user_guide/pipeline-editor-configuration/pipeline-editor-kfp-only-in-jupyterlab-launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bb8282fa881bed73e6df2e118a4e404c65a69e9f GIT binary patch literal 8485 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 TrK@1V00000NkvXXu0mjfSy5aI literal 0 HcmV?d00001 diff --git a/docs/source/images/user_guide/pipeline-editor-configuration/pipeline-editor-subset-in-jupyterlab-launcher.png b/docs/source/images/user_guide/pipeline-editor-configuration/pipeline-editor-subset-in-jupyterlab-launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cb7e443294eabff5e7e71482311d24c78a4262df GIT binary patch literal 12571 zcmaibRajh2uOj3yAKxJUC#XH;XK{%yAM6x zyJt&HckS*~Yt=+3{*XjL#6<)E04UN@V#<(u6;jd>;2@uYF%KP(3h1mXDFUdS#Qz65 z5Hr)1HkX$N&_LP<0AQFE0Or3_AO#mv008KGAOIRt1OMyGhx)&7SM#C&ubuwiiH77a zdjJ4ah_slnst52a2i_w^d~rzOrx!xbXPyF1B_Jj>Vt6DX3C(?Iq)#>MOJ!A6lhc3g2{1mx!?%`(Z#5nIo#?utH^TD{B zSZyMg6TSmJl?LkLD7$OgZ?Heuf5#s>vkD;=8nXuo--Kw`;2i?~uLt`7(<7wn*+6Wu zKb6RTVUlo47hyZ6QW(pp*WmXg=G0o98V*N4uyo6GwP2&3tL0$Di>mqwH5qTZ0pum? zgyl;=Go(+9uA|b>o953m!DrKV#@ z$f}kFBr@MoM-Vvu5o^659W&?<2E9L7U$`u`7IJneO!J$F1TFC{-jj0^OC;*r4q!# ztr0`6v>FyAaPBnncWV~5(K2vUDA+G9DmPOBv~}`F%|S(*sit6 zgC2L_SN^!$3k#{v z$oKtAb0RtAoyJJ+U4aCj!8dyWsDd&Nux}3@=aFacU5D6Nu7U=mO z<&$r(Nb{qlte9BvUU~l6?+wN;tbz$d>b7aUWw~_hHj7QvAuhTtwi)Z7m$O-jK@l-A z?2fOX?k4MnQGngqYV!fMvN1zEScsZiktW})LY(>eXa~+TG3Wai#|U)qReHoa5BAV% z#f2*jHS#5?VEx0N3JdSc8R!eOam9Wfzr#*;%|`&oX18}59qbA_>z3vc2^3_+pr3P1< zZL&H_N=l?_w43FAm&m5!E;RbS+`~RGNcBL*3&tT|RVaynZ6SJVmMr6G<7C%>Rbgn9 z#1+4~WUo4hQ7K0uIeikETmywjYtDcLY?(1(V?|FPGz0PQbbLG>3c%`D%P0wlicclL?gOJ-D>Lw@^@p6YD1z<6wPn8g3lVnluuU;$ zXndRVVWt3Xeq=;M0)_&gqhNl*v{hLS1qOv&{1;M*;p_b|vy;Ud39l<;hg^tT&(`R0 zGQ>o#wAkrrHCZMkgt7trXA;0dM)*{i(}l4-vL#EJBiXn4d*-JsLO0~@pAC|JcwVT2 zB@dD%=leX~K<64?5L@yrCK9!1X3B>H#E8!J^bAIKf4eof>{L1J_PjfdGY`SQ(ca?D z`8qi@@I4wa!a^rr|cq=<^IITi$m1Y#|e@MQlKCz<+yfRvkWQH*b zuFQDao4S(S66oANBHTb@BU7wztw~CsI*>jo;EWKgRS$q^xQcS-(k109<=t zbW(P|UPndavX6Fs0b2$XDp5q8?oJt2<>yutF?qQxFMc5PduHuJ-cN z+jNo^qZqWY>NHZ4hTZ&&YXC~B#dd`Rn}ex5z2hIh&}^|Z2I;=}R4)D7%LC`E)pWiv z9G@ml9N+bDpoS6M6;uW2?OB8m^vC^zqmn{hx;p9h4xbwp2Hf-B%%>S%c)p`okV}Xq z_2r74ykTr4@oI-3-MyD9qI^x2g(5a3n|w=poSHLh{?%IOUjb&9-pNG(F30%!m+F)@dtt$Zq85tJk+lpD*$QWZcU6pzb zi_pnTQCR=bP!^XP7ohnIu~!&&ZRfqV=j+wb{)$B&dwwTmp7&xnH<<|OP%Ssn9S-$T z#Vp6V73Mwig1u^0jApl#EVgsMrPnhT?pNnlF=$~l+q(ssc6dgK5jfXAs`z0TfbSC< zjZ^#Y+cXbKOGgq1y0hx(cK0g(I@%eOx@g^oj>{ZTc;gW;G^2w{KKy~O(38Qmj3Q1( z-2EoSqG71wU2rWxG09ly0tdr9u21AHbxb@%bkF5GiBJ2i)}GnFzJG1S@owra#o%?f zWgGh@=E^NiVqLDC5@&@vLsNlnLnagWZj6CWZoUL()n?dX>c%HsI8V*ZK%tzNh>N9T zhMYQ3cuue1p(zoIPhF$an(B78iix?-=T3|iAVeW(4LHVgDRL4q2J+@5Kv7Z&^xQn#HE*4ewQ4wDcByKX?czsJR}J1Z!4g(r%K`MZ81?mR18cq%nD z<}BqJBk6gcV?Gp@BC`4=v@5YwSiEzQOi)3U)rO#eL{9Qo&vOK}k2L5S8Qw0#dBKG3lTNdJF#hStVmohLu! z`Wy{S?$+B7k>aK$l@-g2H4%{bn9_bto%zcQv#{=(8gttAyVGrMn%ZEMH&4s<>C83h zzvkV&A9KsBs2!D$=3AM{v-n9vOt3DW85MiDJWqflj$ z(~eLrhpA&bBLE3U3z1IBJFlqK2e3p)PZS$6a5doRB7uYWdg5_ZctmtJ(aR)Hk==A z@YqKwu`%&1$&8UQ#i)HG_osj!XJ_!=?j>{t@TrMnV@K1VTYR|S!0r+L=8bZNulOl~ z@KhbUmzkFXF)Hr{j+Wg8kl|&XwiPB^t*+uRuy~qaR&8(;LAmHliG>ci`qM8&tCy;K zI_r?nWk@^Wt-GG+NUZt9m7fxw#-Psa&_i40UUH)DZL~=H365#3R5|0pj^zA^OA#0K z5z~m*74KGzfFgRf)UWK^B(ruVPR_C!$npWQzY<7%@vLDzDdTOKG zv}!cgnUYPPD=-!EE9H$)ngrh;MX?xksZN%DaSE8zYd6JD=J3iI7an7mkm-(rKHl85 z>WxFLSZ$~C1Dfo&^tGF<)kc%6G;ZmS?YlgZhI6a={J-Y_ z&+E@Z`2xoH!qy9s7!?)%m085fl0d}YN54GP#w*8H>R@nxibJ47gwOqoMT5yuEL7a5 z?4Ma|RwRen&at;gGe!MA2F=zfuZMyk3I8EpgCFR9!fg8dN8WeG_yzdO+8#+s&LH7#fx^Obp^v|3S@xHT!OQBEMJ1fHF2fqY1WDD+J2w*R~(iaNjSR zL`@7VRB4PsaN2jB)}QQ?zui`$&)KEg7`tDUAsCv@WH35`D+g=mXd6Ettwp2P@I{lu zR7dUG39Ub1!!5-_6C+L)v#te-R^7(b(1UDU|7@;i?KmyP?{Pow%yV12WAWFOvsu}D z;)&R>5%ni!*}0m?uWmvwcS{Cl$MY4i&W-W9dhIUE#Od75rE~^e>)G8mlf1L_CXy8@ zlHP0I(G4@1j6{ILiHV8D3V8w&UIikP>CAtnS`0u>;$4rsNH{E}QsLAZQ-=Pwzn-(# zDk8Zfs}Cv>*i=*minR;LPPheSr6_tDy;N$eH4JMM4e9j`)8;2YDPC=G4jGw>#f4v} zrB{ogmk|tTh3(aa5qa}{?-n!$NeQH#XQe4e2EG~C(g=1CFd6zs<8e;juEp~JuER-V zsp8#2u80;KHribqOh?d3M~;_%F>-GCG_w7yKB)1rUaHOMxL>t7cxv4O(NgAm6ed}x zatp^IiX)nUFHi#|bzr6)*5Lg$VkSP%#nwvIzN! z@5GnpIV6@@q4oqqPr&eNCc6!m_4NV+K84S`Xf)C75EIC7-5>PIFRA}dH&-5jqve@T zLZ^(#1T3+{gvPD$2M}CwqxV!9U&n4O?G4A9RFHlS`YE?=g z+w6s3@iiKCNERD_y}qy<+-ep{biH@et&~(2$4v1`#W~#Pa)QkA!Z4>b83MK-!AtZA z!C&#ETD=GG_evsvQyEVPiht6QZZ_!gMyDODkv@+w^)YZKxW#S_YiwG1*&A)ZqC6%8 zu?6e+&M1b+;4K%vML5i4)FR|pVK;utXSFMZvcZEOI>t$*GoR}t%qy1jBIGAe=~x_< zwJzQzsXqtOYE$Dx>Ow&S5OkhK1bP(4uGatKRs%ujq!QKzBu;24sH{Fyx%?B`?Ec6p z@jmLVWRrg`x@hN0<&z-JfJ>d3nri+G0@z<}_rl6Xhh&0W5yKwfOpIzy_I>Xf1jPh0 z>790sJeTS#X_Q%R#}G+-cutq8a29EQRSGbHiflJ?$sb}Y>8M8pJTGT8J3Pm*5|h z!b~I2j;mb?3N3cJ7VGkB-bs{<2%aXtP};^jUf3tHUg%L*eGDfDFC#qNtS~#h{bNon zKR#ODe_}+)H7(gd;f;VRa|mgFyI-5wW~L$B{O%tlpmnj?Sz^!)5=J412eS}+A7oe{ z8!9QBEL2gJWjjT_-mlpY^1=Wp$;i?lSc?5uk02|e(1NI5|07Uy^O#a% zkR-a@VxtyH1v-ADnRKtk*|;PW>L|_3+ibqEA~83Iwu>vzhuJ#~x!pO0&)Zc6XPI9q z^dTk{3&r>PIZdmJS$( zONGVxso|djJfA6YHe^$!Scrfr+L3@9q(_WvXxAuRrJ{8x+QX6fLG9E!ys{tH}1 zYHiSJKzxrePK{8T=PXhF}HqJalca@Y_RPQ$MG}ES* z8lYu0_v`%XTMb4=`7GTu7ADmU)KhSg!$)m`6G#x#U1myQm5$M`L0tGixQH@QK}p`v zVI3ZC|MW#;q`@^qBGD^QHDb>C6WhEv8IHu)d2!kihk7SfFVuV%P|+-MoDc1WTxBFC zU65jNyArc(W?ibsVue=mIv3qybiI657KQtVMUdFn@5KOTv1$bY4U-X3j5@xxuT_)v zt3VX#``bB%3(Ap>W8($t%*%;Sn0`{6cYD*!Y+#0z59(nu+8KxDNacko<3I;?FzMbw zqj52)20^RsyGkv78qHd>A67k)cF+yEMhqE!=Fn)~l#L~yQeaqR$Daj6OLaZuWK&IM zf)sjq+=_*(-Ax{*8eHN*6xo(IIJZkP7%M5_+FxoIzGVjjD=oB=wZ5R5YR$42I4bNY zlJ2hjQf(50-=!5z_Nkm-6;-n%LHc(7SO_fZW!Yr3TgQvF3B&(rRyr#2(PM1LGVA56 zv3|R|U08CTmGiBw^Re%L*z0RcVZkJ|$o%a#n9=-kqUA~nR!7Q*4U868Xl}w#AX3%tb!XhtBBPfb2Z{5pn zGyhgY&@Qpez`&R;h4QYPTIJk^+!sw&wT@Y5x98Q#PpCEXy1$&loa89rd*x(~DDHnZ zZ9fuHug8oJ_=n^j!KOM9E0B4IT9Nbl`;t|6wbBN{N(Z}2Eg`RLEfKFyZMR?SsoYrN zBt6)4c3GXp%fYvIIe@KcZ|TH*WafG|-CViV{EN9XUIltw7~OPWz<5lOn!dbATVT}7 z-rZRB{?%*%^XxC1WYc3Wm!)+<9jw{nfI3{#VKCwzNf6#c=)A^lUO z(YKZ3isiaORjX)mI81^)^}E{gqmjV)&9@dIoksH*xeS&8I-Q{~x zGA~8XlIjUZUM{)~$UH8OE%s{!Kc1#$OXcy?64G&N~%U%uZQvZr0ye z`F=!3_72>%j*gI)j{R*V_`YN?`{Rc|WvN^Tr8vdMZxsm*M{wAifH$&iKCsyD{U*;7 z&&4}6(}W%kfPiqR(jQDjGi-`o#U|pNkAf*$OghYhqZ^1s~hTv4>j8eFG zRs~zP=_?l10iuJS38>WUW)2^Nvo&@dPnZ`qDMUP{{-K$V$5p>FE0_U+|7gn{0KpOY z0GNNFh)-pLZxx!MXuDTI*!||N#*FD)7I(&c&vw5@8T&3Lgx`3WuW$7vt&239Dsv0W zUtbG9jiI6P<}-sm!GkfS3WMly8v34Qy)vp&?U2SmH6_$#x)sW0haq)!G6K?5lWrb@nTI@M%~jv zmOXAn&>m8<42SLZmro&0ruukak+XZxVN1ca1dtG)xZ1zPVv4Y_7(mpw`p~plg%^`fnB}N(Q?|?6C4YT@hHVo!x{|b zPmUW89~4d}-=mlqPF?C6PsR&92%wWMAuusPmIGIG>$rzjd?_=@;=u@A_|Rqg|ragNT z?zK9#*v!>xA=9~fd#EK$YwtU$9WettWxW)g8cE22z#Ad4KX76KVRxNwXC$+LrapM| z!#plWio0ez@u{a+xYd4G2)I4N9WBRlwsyR4OZc8{PNBM9B2C!~l%v(3oVJ4Htds>s zY6VFLZA)CH@cy+ATUD=+xR1bf>)tNw+ZBt2-bbg-l!<3?P5l^+pE@u-i9SVWvlNO( z=9e*@-)JP0+-Nw*d8m@hZ26JfCTo#EBLZ{`tp%|k=W1O)^Nux(P(9A$JhfEujEakD z507Yo=+QRT)fl*oN_0U(aw}b|DqS8mc^!hTHS5=xd{3880<$&FM=u8{;y7XC)2Cr7 zGg?f|)!|))f|BCS{t;Pps?Wy|O2Dyx%6woZg0GS&kkhuJaRsitR6__-^dN*|

n60Eg!)F=l<~-(Y~jG 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 cQ1cM@e* literal 0 HcmV?d00001 diff --git a/docs/source/index.rst b/docs/source/index.rst index 8e7eb590b..6d8ac7cc8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -44,6 +44,7 @@ Elyra is a set of AI-centric extensions to JupyterLab Notebooks. user_guide/jupyterlab-interface.md user_guide/command-line-interface.md + user_guide/pipeline-editor-configuration.md user_guide/runtime-conf.md user_guide/runtime-image-conf.md user_guide/pipelines.md diff --git a/docs/source/user_guide/jupyterlab-interface.md b/docs/source/user_guide/jupyterlab-interface.md index 7d5619fbc..7056f521e 100644 --- a/docs/source/user_guide/jupyterlab-interface.md +++ b/docs/source/user_guide/jupyterlab-interface.md @@ -24,7 +24,7 @@ Many of these tasks can also be accomplished using the [Elyra command line inter ### Launcher -Elyra adds a new category to the JupyterLab launcher, providing access to the Visual Pipeline Editor, the [Python editor](enhanced-script-support.html#python-script-execution-support), the [R editor](enhanced-script-support.html#r-script-execution-support), and the [Elyra documentation](https://elyra.readthedocs.io/en/latest/). +Elyra adds a new category to the JupyterLab launcher, providing access to the [Visual Pipeline Editor](#visual-pipeline-editor), the [Python editor](enhanced-script-support.html#python-script-execution-support), the [R editor](enhanced-script-support.html#r-script-execution-support), and the [Elyra documentation](https://elyra.readthedocs.io/en/latest/). ![Elyra category in JupyterLab launcher](../images/user_guide/jupyterlab-interface/launcher.png) @@ -44,7 +44,7 @@ The canvas is the main work area, where you [assemble the pipeline by adding nod The properties panel is used to configure pipeline properties and node properties. -You can customize the pipeline editor behavior by opening the settings link in the empty editor window or by navigating in the [JupyterLab menu bar](https://jupyterlab.readthedocs.io/en/stable/user/interface.html#menu-bar) to `Settings > Advanced Settings Editor` and searching for `elyra`. +Refer to the [_Configuring the pipeline editor_ topic](pipeline-editor-configuration.md) to learn about customizing the editor. ### Metadata management sidebars diff --git a/docs/source/user_guide/pipeline-editor-configuration.md b/docs/source/user_guide/pipeline-editor-configuration.md new file mode 100644 index 000000000..c9a04cea4 --- /dev/null +++ b/docs/source/user_guide/pipeline-editor-configuration.md @@ -0,0 +1,91 @@ + + +## Configuring the pipeline editor + +### Configuring supported runtimes + +The pipeline editor supports three runtimes: Kubeflow Pipelines, Apache Airflow, and local execution in JupyterLab. By default, support for all runtimes is enabled when you [install Elyra](../getting_started/installation.md). The JupyterLab launcher window under the _Elyra_ category includes a tile for each enabled runtime: + +![Pipeline editor tiles in the JupyterLab launcher](../images/user_guide/pipeline-editor-configuration/pipeline-editor-tiles-in-jupyterlab-launcher.png) + +If you are planning to use only one runtime to execute pipelines, you can disable the other runtimes and hide their launcher tiles. + +#### Disabling runtimes + +Runtimes can be disabled by overriding the Elyra default configuration. + +##### Override default using command line parameters + +To disable one or more runtimes, launch JupyterLab with the Elyra-specific `--PipelineProcessorRegistry.runtimes` parameter: + +``` +$ jupyter lab --PipelineProcessorRegistry.runtimes= +``` + +Supported parameter values for `` are `kfp` (enable support for Kubeflow Pipelines), `airflow` (enable support for Apache Airflow), and `local` (enable support for local execution). + +For example, to enable only support for Kubeflow Pipelines, run + +``` +$ jupyter lab --PipelineProcessorRegistry.runtimes=kfp +``` + +![Kubeflow Pipelines editor tile in the JupyterLab launcher](../images/user_guide/pipeline-editor-configuration/pipeline-editor-kfp-only-in-jupyterlab-launcher.png) + + +To enable support for more than one runtime, specify the parameter multiple times. + +``` +$ jupyter lab --PipelineProcessorRegistry.runtimes=kfp --PipelineProcessorRegistry.runtimes=local +``` + +![Kubeflow Pipelines and generic editor tiles in the JupyterLab launcher](../images/user_guide/pipeline-editor-configuration/pipeline-editor-subset-in-jupyterlab-launcher.png) + +##### Override default using customized configuration file + +To permanently apply your runtime selection create a customized configuration file. + +1. Stop JupyterLab. +1. Generate the `jupyter_elyra_config.py` configuration file. + + ``` + $ jupyter elyra --generate-config + ``` + > Note: You must specify `elyra` as the `jupyter` subcommand instead of `lab`. +1. Open the generated configuration file. +1. Locate the `PipelineProcessorRegistry` configuration section. + ``` + #------------------------------------------------------------------------------ + # PipelineProcessorRegistry(SingletonConfigurable) configuration + #------------------------------------------------------------------------------ + ``` +1. Locate the configuration entry for `PipelineProcessorRegistry.runtimes` + ``` + # c.PipelineProcessorRegistry.runtimes = [] + ``` +1. Remove the leading `#` and add one or more of `kfp`,`airflow`, or `local`. + ``` + c.PipelineProcessorRegistry.runtimes = ['kfp', 'local'] + ``` +1. Save the customized configuration file. +1. Start JupyterLab. The pipeline editor tiles for the specified runtimes are displayed in the launcher window. + +### Customizing the pipeline editor + +You can customize the pipeline editor behavior by opening the settings link in the empty editor window or by navigating in the [JupyterLab menu bar](https://jupyterlab.readthedocs.io/en/stable/user/interface.html#menu-bar) to `Settings > Advanced Settings Editor` and searching for `elyra`. Customization options vary by release. \ No newline at end of file From eed5e88dc3464bed13047e6815cc382ac6feef2b Mon Sep 17 00:00:00 2001 From: Patrick Titzler Date: Wed, 22 Feb 2023 15:27:42 -0800 Subject: [PATCH 06/25] Only enable KFP runtime in kf-notebook container image Signed-off-by: Patrick Titzler --- etc/docker/kubeflow/Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/etc/docker/kubeflow/Dockerfile b/etc/docker/kubeflow/Dockerfile index a2f5612a9..1132bf2fe 100644 --- a/etc/docker/kubeflow/Dockerfile +++ b/etc/docker/kubeflow/Dockerfile @@ -39,6 +39,10 @@ RUN python3 -m pip install --upgrade pip # Install Elyra RUN python3 -m pip install --quiet --no-cache-dir "$ELYRA_PACKAGE""$ELYRA_EXTRAS" +# Create and customize Elyra config file to enable only KFP runtime +RUN jupyter elyra --generate-config +RUN sed -i -e "s/# c.PipelineProcessorRegistry.runtimes = \[\]/c.PipelineProcessorRegistry.runtimes = \['kfp'\]/g" $(jupyter --config-dir)/jupyter_elyra_config.py + # Cleanup USER root RUN rm -f README.md ${ELYRA_PACKAGE} From 41ef429bbda98713fd0db49914671ab919316332 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Wed, 22 Feb 2023 17:42:30 -0600 Subject: [PATCH 07/25] Fix local requests bug Signed-off-by: Martha Cryan --- packages/pipeline-editor/src/PipelineEditorWidget.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/pipeline-editor/src/PipelineEditorWidget.tsx b/packages/pipeline-editor/src/PipelineEditorWidget.tsx index 0029d50e2..e1492a2ec 100644 --- a/packages/pipeline-editor/src/PipelineEditorWidget.tsx +++ b/packages/pipeline-editor/src/PipelineEditorWidget.tsx @@ -156,9 +156,15 @@ class PipelineEditorWidget extends ReactWidget { this.refreshPaletteSignal = options.refreshPaletteSignal; this.context = options.context; this.settings = options.settings; + this.context.model.contentChanged.connect(() => { + this.update(); + }); } render(): any { + if (this.context.model.toJSON() === null) { + return

; + } return ( = ({ }) => { const ref = useRef(null); const [loading, setLoading] = useState(true); - const [pipeline, setPipeline] = useState(null); + const [pipeline, setPipeline] = useState(context.model.toJSON()); const [panelOpen, setPanelOpen] = React.useState(false); const type: string | undefined = From bf1ac09348c8d6c71808fb70a94d8255610d404b Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Wed, 22 Feb 2023 18:02:51 -0800 Subject: [PATCH 08/25] Tolerate empty set of elyra_owned_properties when only local runtime is enabled Signed-off-by: Kevin Bates --- elyra/templates/components/generic_properties_template.jinja2 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/elyra/templates/components/generic_properties_template.jinja2 b/elyra/templates/components/generic_properties_template.jinja2 index ad4e59d95..c500c4d6c 100644 --- a/elyra/templates/components/generic_properties_template.jinja2 +++ b/elyra/templates/components/generic_properties_template.jinja2 @@ -115,8 +115,9 @@ "ui:placeholder": "*.csv" } } - }, + } {% if elyra_owned_properties %} + , "additional_properties_header": { "type": "null", "title": "Additional Properties", From 2cffca677aa8f1ba5bc1c40f848354caa807ada6 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Thu, 23 Feb 2023 13:57:31 -0600 Subject: [PATCH 09/25] Update tests with empty pipeline population Signed-off-by: Martha Cryan --- tests/integration/pipeline.ts | 129 +++++++++++++++++++++------------- tests/support/commands.ts | 55 ++++++++------- 2 files changed, 109 insertions(+), 75 deletions(-) diff --git a/tests/integration/pipeline.ts b/tests/integration/pipeline.ts index a7beb4ef7..af274a15e 100644 --- a/tests/integration/pipeline.ts +++ b/tests/integration/pipeline.ts @@ -14,6 +14,28 @@ * limitations under the License. */ +const emptyPipeline = `{ + "doc_type": "pipeline", + "version": "3.0", + "json_schema": "http://api.dataplatform.ibm.com/schemas/common-pipeline/pipeline-flow/pipeline-flow-v3-schema.json", + "id": "elyra-auto-generated-pipeline", + "primary_pipeline": "primary", + "pipelines": [ + { + "id": "primary", + "nodes": [], + "app_data": { + "ui_data": { + "comments": [] + }, + "version": 8 + }, + "runtime_ref": "" + } + ], + "schemas": [] +}`; + describe('Pipeline Editor tests', () => { beforeEach(() => { cy.deleteFile('generic-test.yaml'); // previously exported pipeline @@ -92,45 +114,45 @@ describe('Pipeline Editor tests', () => { // closePipelineEditor(); // }); - it('should block unsupported files', () => { - cy.createPipeline(); - cy.dragAndDropFileToPipeline('invalid.txt'); + // it('should block unsupported files', () => { + // cy.createPipeline({ emptyPipeline }); + // cy.dragAndDropFileToPipeline('invalid.txt'); - // check for unsupported files dialog message - cy.findByText(/unsupported file/i).should('be.visible'); + // // check for unsupported files dialog message + // cy.findByText(/unsupported file/i).should('be.visible'); - // dismiss dialog - cy.contains('OK').click(); - }); + // // dismiss dialog + // cy.contains('OK').click(); + // }); - it('populated editor should have enabled buttons', () => { - cy.createPipeline(); - - cy.checkTabMenuOptions('Pipeline'); - - cy.addFileToPipeline('helloworld.ipynb'); // add Notebook - cy.addFileToPipeline('helloworld.py'); // add Python Script - cy.addFileToPipeline('helloworld.r'); // add R Script - - // check buttons - const disabledButtons = [/redo/i, /cut/i, /copy/i, /paste/i, /delete/i]; - checkDisabledToolbarButtons(disabledButtons); - - const enabledButtons = [ - /run pipeline/i, - /save pipeline/i, - /export pipeline/i, - /clear/i, - /open runtimes/i, - /open runtime images/i, - /open component catalogs/i, - /undo/i, - /add comment/i, - /arrange horizontally/i, - /arrange vertically/i - ]; - checkEnabledToolbarButtons(enabledButtons); - }); + // it('populated editor should have enabled buttons', () => { + // cy.createPipeline({ emptyPipeline }); + + // cy.checkTabMenuOptions('Pipeline'); + + // cy.addFileToPipeline('helloworld.ipynb'); // add Notebook + // cy.addFileToPipeline('helloworld.py'); // add Python Script + // cy.addFileToPipeline('helloworld.r'); // add R Script + + // // check buttons + // const disabledButtons = [/redo/i, /cut/i, /copy/i, /paste/i, /delete/i]; + // checkDisabledToolbarButtons(disabledButtons); + + // const enabledButtons = [ + // /run pipeline/i, + // /save pipeline/i, + // /export pipeline/i, + // /clear/i, + // /open runtimes/i, + // /open runtime images/i, + // /open component catalogs/i, + // /undo/i, + // /add comment/i, + // /arrange horizontally/i, + // /arrange vertically/i + // ]; + // checkEnabledToolbarButtons(enabledButtons); + // }); it('matches complex pipeline snapshot', () => { cy.bootstrapFile('pipelines/consumer.ipynb'); @@ -142,7 +164,10 @@ describe('Pipeline Editor tests', () => { // Do this all manually because our command doesn't support directories yet cy.openDirectory('pipelines'); - cy.writeFile('build/cypress-tests/pipelines/complex.pipeline', ''); + cy.writeFile( + 'build/cypress-tests/pipelines/complex.pipeline', + emptyPipeline + ); cy.openFile('complex.pipeline'); cy.get('.common-canvas-drop-div'); // wait an additional 300ms for the list of items to settle @@ -268,7 +293,7 @@ describe('Pipeline Editor tests', () => { }); it('matches empty pipeline snapshot', () => { - cy.createPipeline({ name: 'empty.pipeline' }); + cy.createPipeline({ name: 'empty.pipeline', emptyPipeline }); cy.addFileToPipeline('helloworld.ipynb'); @@ -283,7 +308,7 @@ describe('Pipeline Editor tests', () => { }); it('matches simple pipeline snapshot', () => { - cy.createPipeline({ name: 'simple.pipeline' }); + cy.createPipeline({ name: 'simple.pipeline', emptyPipeline }); cy.addFileToPipeline('helloworld.ipynb'); @@ -314,7 +339,10 @@ describe('Pipeline Editor tests', () => { // Open a pipeline in a subfolder cy.bootstrapFile('pipelines/producer.ipynb'); cy.openDirectory('pipelines'); - cy.writeFile('build/cypress-tests/pipelines/complex.pipeline', ''); + cy.writeFile( + 'build/cypress-tests/pipelines/complex.pipeline', + emptyPipeline + ); cy.openFile('complex.pipeline'); cy.get('.common-canvas-drop-div'); cy.wait(300); @@ -348,7 +376,10 @@ describe('Pipeline Editor tests', () => { // Open a pipeline in a subfolder cy.bootstrapFile('pipelines/producer.ipynb'); cy.openDirectory('pipelines'); - cy.writeFile('build/cypress-tests/pipelines/complex.pipeline', ''); + cy.writeFile( + 'build/cypress-tests/pipelines/complex.pipeline', + emptyPipeline + ); cy.openFile('complex.pipeline'); cy.get('.common-canvas-drop-div'); cy.wait(300); @@ -364,7 +395,7 @@ describe('Pipeline Editor tests', () => { }); it('should save runtime configuration', () => { - cy.createPipeline(); + cy.createPipeline({ emptyPipeline }); // Create kfp runtime configuration cy.createRuntimeConfig({ type: 'kfp' }); @@ -391,7 +422,7 @@ describe('Pipeline Editor tests', () => { // TODO: Investigate CI failures commented below // it('should run pipeline after adding runtime image', () => { - // cy.createPipeline(); + // cy.createPipeline({ emptyPipeline }); // cy.addFileToPipeline('helloworld.ipynb'); // add Notebook @@ -651,7 +682,7 @@ describe('Pipeline Editor tests', () => { it('kfp pipeline should display custom components', () => { cy.createExampleComponentCatalog({ type: 'kfp' }); - cy.createPipeline({ type: 'kfp' }); + cy.createPipeline({ type: 'kfp', emptyPipeline }); cy.get('.palette-flyout-category[value="examples"]').click(); const kfpCustomComponents = [ @@ -667,7 +698,7 @@ describe('Pipeline Editor tests', () => { }); it('kfp pipeline should display expected export options', () => { - cy.createPipeline({ type: 'kfp' }); + cy.createPipeline({ type: 'kfp', emptyPipeline }); cy.savePipeline(); cy.installRuntimeConfig({ type: 'kfp' }); @@ -682,7 +713,7 @@ describe('Pipeline Editor tests', () => { }); it('airflow pipeline should display expected export options', () => { - cy.createPipeline({ type: 'airflow' }); + cy.createPipeline({ type: 'airflow', emptyPipeline }); cy.savePipeline(); cy.installRuntimeConfig({ type: 'airflow' }); @@ -706,7 +737,7 @@ describe('Pipeline Editor tests', () => { }); it('exporting generic pipeline with invalid runtime config should produce request error', () => { - cy.createPipeline(); + cy.createPipeline({ emptyPipeline }); cy.savePipeline(); cy.installRuntimeConfig(); @@ -722,7 +753,7 @@ describe('Pipeline Editor tests', () => { }); it('generic pipeline should display expected export options', () => { - cy.createPipeline(); + cy.createPipeline({ emptyPipeline }); cy.savePipeline(); // Test Airflow export options @@ -753,7 +784,7 @@ describe('Pipeline Editor tests', () => { }); it('generic pipeline toolbar should display expected runtime', () => { - cy.createPipeline(); + cy.createPipeline({ emptyPipeline }); cy.get('.toolbar-icon-label').contains(/runtime: generic/i); }); diff --git a/tests/support/commands.ts b/tests/support/commands.ts index 019c5851f..2710a205d 100644 --- a/tests/support/commands.ts +++ b/tests/support/commands.ts @@ -135,34 +135,37 @@ Cypress.Commands.add('deleteFile', (name: string): void => { }); }); -Cypress.Commands.add('createPipeline', ({ name, type } = {}): void => { - if (name === undefined) { - switch (type) { - case 'kfp': - cy.get( - '.jp-LauncherCard[data-category="Elyra"][title="Kubeflow Pipelines Pipeline Editor"]' - ).click(); - break; - case 'airflow': - cy.get( - '.jp-LauncherCard[data-category="Elyra"][title="Apache Airflow Pipeline Editor"]' - ).click(); - break; - default: - cy.get( - '.jp-LauncherCard[data-category="Elyra"][title="Generic Pipeline Editor"]' - ).click(); - break; +Cypress.Commands.add( + 'createPipeline', + ({ name, type, emptyPipeline } = {}): void => { + if (name === undefined) { + switch (type) { + case 'kfp': + cy.get( + '.jp-LauncherCard[data-category="Elyra"][title="Kubeflow Pipelines Pipeline Editor"]' + ).click(); + break; + case 'airflow': + cy.get( + '.jp-LauncherCard[data-category="Elyra"][title="Apache Airflow Pipeline Editor"]' + ).click(); + break; + default: + cy.get( + '.jp-LauncherCard[data-category="Elyra"][title="Generic Pipeline Editor"]' + ).click(); + break; + } + } else { + cy.writeFile(`build/cypress-tests/${name}`, emptyPipeline ?? ''); + cy.openFile(name); } - } else { - cy.writeFile(`build/cypress-tests/${name}`, ''); - cy.openFile(name); - } - cy.get('.common-canvas-drop-div'); - // wait an additional 300ms for the list of items to settle - cy.wait(300); -}); + cy.get('.common-canvas-drop-div'); + // wait an additional 300ms for the list of items to settle + cy.wait(300); + } +); Cypress.Commands.add('openDirectory', (name: string): void => { cy.findByRole('listitem', { From d1a2c29b3d3050d2c2f793af717d47b1db70a872 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Thu, 23 Feb 2023 14:02:45 -0600 Subject: [PATCH 10/25] Uncomment tests Signed-off-by: Martha Cryan --- tests/integration/pipeline.ts | 70 +++++++++++++++++------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/tests/integration/pipeline.ts b/tests/integration/pipeline.ts index af274a15e..26d0ca5bc 100644 --- a/tests/integration/pipeline.ts +++ b/tests/integration/pipeline.ts @@ -114,45 +114,45 @@ describe('Pipeline Editor tests', () => { // closePipelineEditor(); // }); - // it('should block unsupported files', () => { - // cy.createPipeline({ emptyPipeline }); - // cy.dragAndDropFileToPipeline('invalid.txt'); - - // // check for unsupported files dialog message - // cy.findByText(/unsupported file/i).should('be.visible'); - - // // dismiss dialog - // cy.contains('OK').click(); - // }); - - // it('populated editor should have enabled buttons', () => { - // cy.createPipeline({ emptyPipeline }); + it('should block unsupported files', () => { + cy.createPipeline({ emptyPipeline }); + cy.dragAndDropFileToPipeline('invalid.txt'); - // cy.checkTabMenuOptions('Pipeline'); + // check for unsupported files dialog message + cy.findByText(/unsupported file/i).should('be.visible'); - // cy.addFileToPipeline('helloworld.ipynb'); // add Notebook - // cy.addFileToPipeline('helloworld.py'); // add Python Script - // cy.addFileToPipeline('helloworld.r'); // add R Script + // dismiss dialog + cy.contains('OK').click(); + }); - // // check buttons - // const disabledButtons = [/redo/i, /cut/i, /copy/i, /paste/i, /delete/i]; - // checkDisabledToolbarButtons(disabledButtons); + it('populated editor should have enabled buttons', () => { + cy.createPipeline({ emptyPipeline }); - // const enabledButtons = [ - // /run pipeline/i, - // /save pipeline/i, - // /export pipeline/i, - // /clear/i, - // /open runtimes/i, - // /open runtime images/i, - // /open component catalogs/i, - // /undo/i, - // /add comment/i, - // /arrange horizontally/i, - // /arrange vertically/i - // ]; - // checkEnabledToolbarButtons(enabledButtons); - // }); + cy.checkTabMenuOptions('Pipeline'); + + cy.addFileToPipeline('helloworld.ipynb'); // add Notebook + cy.addFileToPipeline('helloworld.py'); // add Python Script + cy.addFileToPipeline('helloworld.r'); // add R Script + + // check buttons + const disabledButtons = [/redo/i, /cut/i, /copy/i, /paste/i, /delete/i]; + checkDisabledToolbarButtons(disabledButtons); + + const enabledButtons = [ + /run pipeline/i, + /save pipeline/i, + /export pipeline/i, + /clear/i, + /open runtimes/i, + /open runtime images/i, + /open component catalogs/i, + /undo/i, + /add comment/i, + /arrange horizontally/i, + /arrange vertically/i + ]; + checkEnabledToolbarButtons(enabledButtons); + }); it('matches complex pipeline snapshot', () => { cy.bootstrapFile('pipelines/consumer.ipynb'); From 60fae551063169c3c13b6153e7ad9ef1b1bcc9b4 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Thu, 23 Feb 2023 17:28:20 -0600 Subject: [PATCH 11/25] Integration test fix Signed-off-by: Martha Cryan --- tests/integration/pipeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/pipeline.ts b/tests/integration/pipeline.ts index 26d0ca5bc..2cd1751dd 100644 --- a/tests/integration/pipeline.ts +++ b/tests/integration/pipeline.ts @@ -219,7 +219,7 @@ describe('Pipeline Editor tests', () => { }); // consumer props - cy.findByText('consumer.ipynb').click(); + cy.findByText('consumer.ipynb').click({ force: true }); cy.get('#root_component_parameters_runtime_image').within(() => { cy.get('select[id="root_component_parameters_runtime_image"]').select( 'continuumio/anaconda3@sha256:a2816acd3acda208d92e0bf6c11eb41fda9009ea20f24e123dbf84bb4bd4c4b8' From 97e60e2965583023676d7c0034442ed329bfb719 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Thu, 23 Feb 2023 17:52:10 -0600 Subject: [PATCH 12/25] Integration test fix Signed-off-by: Martha Cryan --- tests/integration/pipeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/pipeline.ts b/tests/integration/pipeline.ts index 2cd1751dd..e53da6ead 100644 --- a/tests/integration/pipeline.ts +++ b/tests/integration/pipeline.ts @@ -227,7 +227,7 @@ describe('Pipeline Editor tests', () => { }); // setup props - cy.findByText('setup.py').click(); + cy.findByText('setup.py').click({ force: true }); cy.get('#root_component_parameters_runtime_image').within(() => { cy.get('select[id="root_component_parameters_runtime_image"]').select( 'continuumio/anaconda3@sha256:a2816acd3acda208d92e0bf6c11eb41fda9009ea20f24e123dbf84bb4bd4c4b8' From db85f72ab65b466c73ca9f54b39934e54570b1ec Mon Sep 17 00:00:00 2001 From: Patrick Titzler Date: Fri, 24 Feb 2023 08:49:20 -0800 Subject: [PATCH 13/25] Address review comments Signed-off-by: Patrick Titzler --- docs/source/user_guide/pipeline-editor-configuration.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/source/user_guide/pipeline-editor-configuration.md b/docs/source/user_guide/pipeline-editor-configuration.md index c9a04cea4..18641d74c 100644 --- a/docs/source/user_guide/pipeline-editor-configuration.md +++ b/docs/source/user_guide/pipeline-editor-configuration.md @@ -24,15 +24,15 @@ The pipeline editor supports three runtimes: Kubeflow Pipelines, Apache Airflow, ![Pipeline editor tiles in the JupyterLab launcher](../images/user_guide/pipeline-editor-configuration/pipeline-editor-tiles-in-jupyterlab-launcher.png) -If you are planning to use only one runtime to execute pipelines, you can disable the other runtimes and hide their launcher tiles. +If you are planning to use only a subset of the supported runtimes to execute pipelines, you can enable them selectively. -#### Disabling runtimes +#### Enabling specific runtimes -Runtimes can be disabled by overriding the Elyra default configuration. +When you explicitly enable one or more runtimes the other runtimes are disabled. You enable runtimes by overriding the Elyra default configuration. ##### Override default using command line parameters -To disable one or more runtimes, launch JupyterLab with the Elyra-specific `--PipelineProcessorRegistry.runtimes` parameter: +To enable one or more runtimes, launch JupyterLab with the Elyra-specific `--PipelineProcessorRegistry.runtimes` parameter: ``` $ jupyter lab --PipelineProcessorRegistry.runtimes= @@ -48,7 +48,6 @@ $ jupyter lab --PipelineProcessorRegistry.runtimes=kfp ![Kubeflow Pipelines editor tile in the JupyterLab launcher](../images/user_guide/pipeline-editor-configuration/pipeline-editor-kfp-only-in-jupyterlab-launcher.png) - To enable support for more than one runtime, specify the parameter multiple times. ``` From 01717d86d8446e1534dba7d65ed93afd2075a9d0 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Fri, 24 Feb 2023 11:56:14 -0600 Subject: [PATCH 14/25] Only update when the pipeline was null the last request Signed-off-by: Martha Cryan --- packages/pipeline-editor/src/PipelineEditorWidget.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/pipeline-editor/src/PipelineEditorWidget.tsx b/packages/pipeline-editor/src/PipelineEditorWidget.tsx index e1492a2ec..3d7457856 100644 --- a/packages/pipeline-editor/src/PipelineEditorWidget.tsx +++ b/packages/pipeline-editor/src/PipelineEditorWidget.tsx @@ -156,8 +156,12 @@ class PipelineEditorWidget extends ReactWidget { this.refreshPaletteSignal = options.refreshPaletteSignal; this.context = options.context; this.settings = options.settings; + let nullPipeline = this.context.model.toJSON() === null; this.context.model.contentChanged.connect(() => { - this.update(); + if (nullPipeline) { + nullPipeline = false; + this.update(); + } }); } From 1274d5bfbe567d41c8c1520198cd7d811d68ba69 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Fri, 24 Feb 2023 12:01:34 -0600 Subject: [PATCH 15/25] Remove force clicks Signed-off-by: Martha Cryan --- tests/integration/pipeline.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/pipeline.ts b/tests/integration/pipeline.ts index e53da6ead..26d0ca5bc 100644 --- a/tests/integration/pipeline.ts +++ b/tests/integration/pipeline.ts @@ -219,7 +219,7 @@ describe('Pipeline Editor tests', () => { }); // consumer props - cy.findByText('consumer.ipynb').click({ force: true }); + cy.findByText('consumer.ipynb').click(); cy.get('#root_component_parameters_runtime_image').within(() => { cy.get('select[id="root_component_parameters_runtime_image"]').select( 'continuumio/anaconda3@sha256:a2816acd3acda208d92e0bf6c11eb41fda9009ea20f24e123dbf84bb4bd4c4b8' @@ -227,7 +227,7 @@ describe('Pipeline Editor tests', () => { }); // setup props - cy.findByText('setup.py').click({ force: true }); + cy.findByText('setup.py').click(); cy.get('#root_component_parameters_runtime_image').within(() => { cy.get('select[id="root_component_parameters_runtime_image"]').select( 'continuumio/anaconda3@sha256:a2816acd3acda208d92e0bf6c11eb41fda9009ea20f24e123dbf84bb4bd4c4b8' From 103085a41d9bddfe62fdaeb08119440ece88228c Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Fri, 24 Feb 2023 11:00:56 -0800 Subject: [PATCH 16/25] Indicate runtime enablement in resources payload Signed-off-by: Kevin Bates --- elyra/pipeline/processor.py | 15 ++++++++++++++- elyra/pipeline/runtime_type.py | 18 +++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/elyra/pipeline/processor.py b/elyra/pipeline/processor.py index 018230a2c..41a646055 100644 --- a/elyra/pipeline/processor.py +++ b/elyra/pipeline/processor.py @@ -136,12 +136,25 @@ def get_runtime_types_resources(self) -> List[RuntimeTypeResources]: # Build set of active runtime types, then build list of resources instances runtime_types: Set[RuntimeProcessorType] = set() + enabled_runtimes: Set[RuntimeProcessorType] = set() # Track which runtimes are enabled for name, processor in self._processors.items(): runtime_types.add(processor.type) + enabled_runtimes.add(processor.type) + + # Unconditionally include "generic" resources since, to this point, all non-local runtimes + # also support generic components, so we need their resources on the frontend, despite the + # fact that the "local runtime" may not be enabled. When it is not enabled, it won't be in + # the runtime_types list, so, in that case, we add it, but not mark it as an enabled runtime. + if RuntimeProcessorType.LOCAL not in runtime_types: + runtime_types.add(RuntimeProcessorType.LOCAL) resources: List[RuntimeTypeResources] = list() for runtime_type in runtime_types: - resources.append(RuntimeTypeResources.get_instance_by_type(runtime_type)) + resources.append( + RuntimeTypeResources.get_instance_by_type( + runtime_type, runtime_enabled=(runtime_type in enabled_runtimes) + ) + ) return resources diff --git a/elyra/pipeline/runtime_type.py b/elyra/pipeline/runtime_type.py index 9b18d7453..d6bf7206e 100644 --- a/elyra/pipeline/runtime_type.py +++ b/elyra/pipeline/runtime_type.py @@ -18,6 +18,7 @@ from typing import Any from typing import Dict from typing import List +from typing import Optional @unique @@ -65,17 +66,23 @@ class RuntimeTypeResources(object): type: RuntimeProcessorType icon_endpoint: str export_file_types: List[Dict[str, str]] + runtime_enabled: bool + + def __init__(self, runtime_enabled: Optional[bool] = True): + self.runtime_enabled = runtime_enabled @classmethod - def get_instance_by_type(cls, runtime_type: RuntimeProcessorType) -> "RuntimeTypeResources": + def get_instance_by_type( + cls, runtime_type: RuntimeProcessorType, runtime_enabled: Optional[bool] = True + ) -> "RuntimeTypeResources": if runtime_type == RuntimeProcessorType.KUBEFLOW_PIPELINES: - return KubeflowPipelinesResources() + return KubeflowPipelinesResources(runtime_enabled=runtime_enabled) if runtime_type == RuntimeProcessorType.APACHE_AIRFLOW: - return ApacheAirflowResources() + return ApacheAirflowResources(runtime_enabled=runtime_enabled) if runtime_type == RuntimeProcessorType.ARGO: - return ArgoResources() + return ArgoResources(runtime_enabled=runtime_enabled) if runtime_type == RuntimeProcessorType.LOCAL: - return LocalResources() + return LocalResources(runtime_enabled=runtime_enabled) raise ValueError(f"Runtime type {runtime_type} is not recognized.") @property @@ -92,6 +99,7 @@ def to_dict(self) -> Dict[str, Any]: display_name=self.display_name, icon=self.icon_endpoint, export_file_types=self.export_file_types, + runtime_enabled=self.runtime_enabled, ) return d From af76cb8fc4ce73cf0bd5a2d40d9883f233054405 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Fri, 24 Feb 2023 16:25:18 -0600 Subject: [PATCH 17/25] Filter runtimes by enabled Signed-off-by: Martha Cryan --- .../src/PipelineEditorWidget.tsx | 17 +++++++++++++---- .../src/SubmitFileButtonExtension.tsx | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/pipeline-editor/src/PipelineEditorWidget.tsx b/packages/pipeline-editor/src/PipelineEditorWidget.tsx index 3d7457856..69ffcd5e0 100644 --- a/packages/pipeline-editor/src/PipelineEditorWidget.tsx +++ b/packages/pipeline-editor/src/PipelineEditorWidget.tsx @@ -657,13 +657,22 @@ const PipelineWrapper: React.FC = ({ ); // TODO: Parallelize this - const runtimes = await PipelineService.getRuntimes().catch(error => - RequestErrors.serverError(error) - ); + const runtimeTypes = await PipelineService.getRuntimeTypes(); + const runtimes = await PipelineService.getRuntimes() + .then(runtimeList => { + return runtimeList.filter((runtime: any) => { + return ( + !runtime.metadata.runtime_enabled && + !!runtimeTypes.find( + (r: any) => runtime.metadata.runtime_type === r.id + ) + ); + }); + }) + .catch(error => RequestErrors.serverError(error)); const schema = await PipelineService.getRuntimesSchema().catch(error => RequestErrors.serverError(error) ); - const runtimeTypes = await PipelineService.getRuntimeTypes(); const runtimeData = createRuntimeData({ schema, diff --git a/packages/pipeline-editor/src/SubmitFileButtonExtension.tsx b/packages/pipeline-editor/src/SubmitFileButtonExtension.tsx index 831bd7c9f..4591ad7be 100644 --- a/packages/pipeline-editor/src/SubmitFileButtonExtension.tsx +++ b/packages/pipeline-editor/src/SubmitFileButtonExtension.tsx @@ -60,9 +60,21 @@ export class SubmitFileButtonExtension< const env = await ContentParser.getEnvVars(context.path).catch(error => RequestErrors.serverError(error) ); - const runtimes = await PipelineService.getRuntimes().catch(error => - RequestErrors.serverError(error) + const runtimeTypes: any = await PipelineService.getRuntimeTypes().catch( + error => RequestErrors.serverError(error) ); + const runtimes = await PipelineService.getRuntimes() + .then(runtimeList => { + return runtimeList.filter((runtime: any) => { + return ( + !runtime.metadata.runtime_enabled && + !!runtimeTypes.find( + (r: any) => runtime.metadata.runtime_type === r.id + ) + ); + }); + }) + .catch(error => RequestErrors.serverError(error)); const images = await PipelineService.getRuntimeImages().catch(error => RequestErrors.serverError(error) ); From 730247c60924eacde1de56f9b683fb576efd226e Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Mon, 27 Feb 2023 16:31:43 -0600 Subject: [PATCH 18/25] Close pipeline on error Signed-off-by: Martha Cryan --- packages/pipeline-editor/src/PipelineEditorWidget.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/pipeline-editor/src/PipelineEditorWidget.tsx b/packages/pipeline-editor/src/PipelineEditorWidget.tsx index 69ffcd5e0..603bbecd4 100644 --- a/packages/pipeline-editor/src/PipelineEditorWidget.tsx +++ b/packages/pipeline-editor/src/PipelineEditorWidget.tsx @@ -250,18 +250,21 @@ const PipelineWrapper: React.FC = ({ useEffect(() => { if (paletteError) { RequestErrors.serverError(paletteError); + shell.currentWidget?.close(); } }, [paletteError]); useEffect(() => { if (runtimeImagesError) { RequestErrors.serverError(runtimeImagesError); + shell.currentWidget?.close(); } }, [runtimeImagesError]); useEffect(() => { if (runtimesSchemaError) { RequestErrors.serverError(runtimesSchemaError); + shell.currentWidget?.close(); } }, [runtimesSchemaError]); From 5352ea07c0712a84f36b6788faded2a297a6d467 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Mon, 27 Feb 2023 18:05:18 -0600 Subject: [PATCH 19/25] Filter runtime configs by runtime types Signed-off-by: Martha Cryan --- .../metadata-common/src/MetadataWidget.tsx | 1 + .../pipeline-editor/src/PipelineService.tsx | 1 + .../pipeline-editor/src/RuntimesWidget.tsx | 50 +++++++++++++++++-- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/metadata-common/src/MetadataWidget.tsx b/packages/metadata-common/src/MetadataWidget.tsx index 0dce90227..e91e94d8a 100644 --- a/packages/metadata-common/src/MetadataWidget.tsx +++ b/packages/metadata-common/src/MetadataWidget.tsx @@ -366,6 +366,7 @@ export class MetadataWidget extends ReactWidget { this.renderSignal = new Signal(this); this.titleContext = props.titleContext; this.fetchMetadata = this.fetchMetadata.bind(this); + this.getSchemas = this.getSchemas.bind(this); this.updateMetadata = this.updateMetadata.bind(this); this.refreshMetadata = this.refreshMetadata.bind(this); this.openMetadataEditor = this.openMetadataEditor.bind(this); diff --git a/packages/pipeline-editor/src/PipelineService.tsx b/packages/pipeline-editor/src/PipelineService.tsx index ece9e7540..fa65ce132 100644 --- a/packages/pipeline-editor/src/PipelineService.tsx +++ b/packages/pipeline-editor/src/PipelineService.tsx @@ -61,6 +61,7 @@ const CONTENT_TYPE_MAPPER: Map = new Map([ ]); export interface IRuntimeType { + runtime_enabled: boolean; id: string; display_name: string; icon: string; diff --git a/packages/pipeline-editor/src/RuntimesWidget.tsx b/packages/pipeline-editor/src/RuntimesWidget.tsx index 5b4bca3f5..df137bb8e 100644 --- a/packages/pipeline-editor/src/RuntimesWidget.tsx +++ b/packages/pipeline-editor/src/RuntimesWidget.tsx @@ -21,11 +21,15 @@ import { IMetadataDisplayProps, IMetadataDisplayState } from '@elyra/metadata-common'; -import { IDictionary } from '@elyra/services'; +import { IDictionary, MetadataService } from '@elyra/services'; import { RequestErrors } from '@elyra/ui-components'; import React from 'react'; -import { PipelineService, RUNTIMES_SCHEMASPACE } from './PipelineService'; +import { + IRuntimeType, + PipelineService, + RUNTIMES_SCHEMASPACE +} from './PipelineService'; const RUNTIMES_METADATA_CLASS = 'elyra-metadata-runtimes'; @@ -133,6 +137,8 @@ class RuntimesDisplay extends MetadataDisplay< * A widget for displaying runtimes. */ export class RuntimesWidget extends MetadataWidget { + runtimeTypes: IRuntimeType[] = []; + constructor(props: IMetadataWidgetProps) { super(props); } @@ -143,6 +149,40 @@ export class RuntimesWidget extends MetadataWidget { ); } + async getSchemas(): Promise { + try { + const schemas = await MetadataService.getSchema(this.props.schemaspace); + this.runtimeTypes = await PipelineService.getRuntimeTypes(); + const sortedSchema = schemas.sort((a: any, b: any) => + a.title.localeCompare(b.title) + ); + this.schemas = sortedSchema.filter((schema: any) => { + return !!this.runtimeTypes.find( + r => r.id === schema.runtime_type && r.runtime_enabled + ); + }); + if (this.schemas?.length ?? 0 > 1) { + for (const schema of this.schemas ?? []) { + this.props.app.contextMenu.addItem({ + selector: `#${this.props.schemaspace} .elyra-metadataHeader-addButton`, + command: 'elyra-metadata-editor:open', + args: { + onSave: this.updateMetadata, + schemaspace: this.props.schemaspace, + schema: schema.name, + title: schema.title, + titleContext: this.props.titleContext, + appendToTitle: this.props.appendToTitle + } as any + }); + } + } + this.update(); + } catch (error) { + RequestErrors.serverError(error); + } + } + private getSchemaTitle = (metadata: IMetadata): string => { if (this.schemas) { for (const schema of this.schemas) { @@ -177,9 +217,13 @@ export class RuntimesWidget extends MetadataWidget { ); } + const filteredMetadata = metadata.filter(m => { + return !!this.runtimeTypes.find(r => m.metadata?.runtime_type === r.id); + }); + return ( Date: Mon, 27 Feb 2023 18:12:19 -0600 Subject: [PATCH 20/25] fix lint Signed-off-by: Martha Cryan --- packages/pipeline-editor/src/PipelineEditorWidget.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/pipeline-editor/src/PipelineEditorWidget.tsx b/packages/pipeline-editor/src/PipelineEditorWidget.tsx index 603bbecd4..d4223b907 100644 --- a/packages/pipeline-editor/src/PipelineEditorWidget.tsx +++ b/packages/pipeline-editor/src/PipelineEditorWidget.tsx @@ -252,21 +252,21 @@ const PipelineWrapper: React.FC = ({ RequestErrors.serverError(paletteError); shell.currentWidget?.close(); } - }, [paletteError]); + }, [paletteError, shell.currentWidget]); useEffect(() => { if (runtimeImagesError) { RequestErrors.serverError(runtimeImagesError); shell.currentWidget?.close(); } - }, [runtimeImagesError]); + }, [runtimeImagesError, shell.currentWidget]); useEffect(() => { if (runtimesSchemaError) { RequestErrors.serverError(runtimesSchemaError); shell.currentWidget?.close(); } - }, [runtimesSchemaError]); + }, [runtimesSchemaError, shell.currentWidget]); const contextRef = useRef(context); useEffect(() => { From 7a2949d7bfd2080d0fdc9bf536118d4d00f02db8 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Mon, 27 Feb 2023 18:28:42 -0600 Subject: [PATCH 21/25] Filter component catalogs by runtime types Signed-off-by: Martha Cryan --- .../src/ComponentCatalogsWidget.tsx | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/pipeline-editor/src/ComponentCatalogsWidget.tsx b/packages/pipeline-editor/src/ComponentCatalogsWidget.tsx index 1a92e4176..bfcec23fd 100644 --- a/packages/pipeline-editor/src/ComponentCatalogsWidget.tsx +++ b/packages/pipeline-editor/src/ComponentCatalogsWidget.tsx @@ -23,14 +23,14 @@ import { IMetadataDisplayState, IMetadataActionButton } from '@elyra/metadata-common'; -import { IDictionary } from '@elyra/services'; +import { IDictionary, MetadataService } from '@elyra/services'; import { RequestErrors } from '@elyra/ui-components'; import { JupyterFrontEnd } from '@jupyterlab/application'; import { LabIcon, refreshIcon } from '@jupyterlab/ui-components'; import React from 'react'; -import { PipelineService } from './PipelineService'; +import { IRuntimeType, PipelineService } from './PipelineService'; export const COMPONENT_CATALOGS_SCHEMASPACE = 'component-catalogs'; @@ -124,6 +124,7 @@ export interface IComponentCatalogsWidgetProps extends IMetadataWidgetProps { export class ComponentCatalogsWidget extends MetadataWidget { refreshButtonTooltip: string; refreshCallback?: () => void; + runtimeTypes: IRuntimeType[] = []; constructor(props: IComponentCatalogsWidgetProps) { super(props); @@ -132,6 +133,43 @@ export class ComponentCatalogsWidget extends MetadataWidget { 'Refresh list and reload components from all catalogs'; } + async getSchemas(): Promise { + try { + const schemas = await MetadataService.getSchema(this.props.schemaspace); + this.runtimeTypes = await PipelineService.getRuntimeTypes(); + const sortedSchema = schemas.sort((a: any, b: any) => + a.title.localeCompare(b.title) + ); + this.schemas = sortedSchema.filter((schema: any) => { + return !!this.runtimeTypes.find( + r => + schema.properties?.metadata?.properties?.runtime_type?.enum?.includes( + r.id + ) && r.runtime_enabled + ); + }); + if (this.schemas?.length ?? 0 > 1) { + for (const schema of this.schemas ?? []) { + this.props.app.contextMenu.addItem({ + selector: `#${this.props.schemaspace} .elyra-metadataHeader-addButton`, + command: 'elyra-metadata-editor:open', + args: { + onSave: this.updateMetadata, + schemaspace: this.props.schemaspace, + schema: schema.name, + title: schema.title, + titleContext: this.props.titleContext, + appendToTitle: this.props.appendToTitle + } as any + }); + } + } + this.update(); + } catch (error) { + RequestErrors.serverError(error); + } + } + // wrapper function that refreshes the palette after calling updateMetadata updateMetadataAndRefresh = (): void => { super.updateMetadata(); @@ -161,9 +199,13 @@ export class ComponentCatalogsWidget extends MetadataWidget { ); } + const filteredMetadata = metadata.filter(m => { + return !!this.runtimeTypes.find(r => m.metadata?.runtime_type === r.id); + }); + return ( Date: Tue, 28 Feb 2023 15:47:24 -0800 Subject: [PATCH 22/25] Check if corresponding runtime is enabled before processing catalog Signed-off-by: Kevin Bates --- elyra/elyra_app.py | 2 +- elyra/pipeline/component_catalog.py | 11 ++- elyra/pipeline/handlers.py | 2 +- elyra/pipeline/processor.py | 102 +------------------- elyra/pipeline/registry.py | 126 +++++++++++++++++++++++++ elyra/tests/pipeline/test_processor.py | 2 +- 6 files changed, 140 insertions(+), 105 deletions(-) create mode 100644 elyra/pipeline/registry.py diff --git a/elyra/elyra_app.py b/elyra/elyra_app.py index c4f494c4d..c28490a4b 100644 --- a/elyra/elyra_app.py +++ b/elyra/elyra_app.py @@ -44,7 +44,7 @@ from elyra.pipeline.handlers import PipelineValidationHandler from elyra.pipeline.processor import PipelineProcessor from elyra.pipeline.processor import PipelineProcessorManager -from elyra.pipeline.processor import PipelineProcessorRegistry +from elyra.pipeline.registry import PipelineProcessorRegistry from elyra.pipeline.validation import PipelineValidationManager diff --git a/elyra/pipeline/component_catalog.py b/elyra/pipeline/component_catalog.py index c3cad4df5..3a0ebfd19 100644 --- a/elyra/pipeline/component_catalog.py +++ b/elyra/pipeline/component_catalog.py @@ -44,6 +44,7 @@ from elyra.pipeline.component import ComponentParser from elyra.pipeline.component_metadata import ComponentCatalogMetadata from elyra.pipeline.properties import ComponentProperty +from elyra.pipeline.registry import PipelineProcessorRegistry from elyra.pipeline.runtime_type import RuntimeProcessorType BLOCKING_TIMEOUT = 0.5 @@ -451,7 +452,7 @@ def update(self, catalog: Metadata, action: str): """ self._insert_request(self.update_queue, catalog, action) - def _insert_request(self, queue: Queue, catalog: Metadata, action: str): + def _insert_request(self, queue: Queue, catalog: ComponentCatalogMetadata, action: str): """ If running as a server process, the request is submitted to the desired queue, otherwise it is posted to the manifest where the server process (if running) can detect the manifest @@ -462,6 +463,10 @@ def _insert_request(self, queue: Queue, catalog: Metadata, action: str): instead, raise NotImplementedError in such cases, but we may want the ability to refresh the entire component cache from a CLI utility and the current implementation would allow that. """ + # Ensure referenced runtime is available + if not PipelineProcessorRegistry.instance().is_valid_runtime_type(catalog.runtime_type.name): + return + if self.is_server_process: queue.put((catalog, action)) else: @@ -575,6 +580,10 @@ def read_component_catalog(self, catalog: ComponentCatalogMetadata) -> Dict[str, """ components: Dict[str, Component] = {} + # Ensure referenced runtime is available + if not PipelineProcessorRegistry.instance().is_valid_runtime_type(catalog.runtime_type.name): + return components + # Assign component parser based on the runtime platform type parser = ComponentParser.create_instance(platform=catalog.runtime_type) diff --git a/elyra/pipeline/handlers.py b/elyra/pipeline/handlers.py index 4ae630582..956747a7a 100644 --- a/elyra/pipeline/handlers.py +++ b/elyra/pipeline/handlers.py @@ -35,7 +35,7 @@ from elyra.pipeline.pipeline_constants import PIPELINE_PARAMETERS from elyra.pipeline.pipeline_definition import PipelineDefinition from elyra.pipeline.processor import PipelineProcessorManager -from elyra.pipeline.processor import PipelineProcessorRegistry +from elyra.pipeline.registry import PipelineProcessorRegistry from elyra.pipeline.runtime_type import RuntimeProcessorType from elyra.pipeline.runtime_type import RuntimeTypeResources from elyra.pipeline.validation import PipelineValidationManager diff --git a/elyra/pipeline/processor.py b/elyra/pipeline/processor.py index 41a646055..7dcf70d4f 100644 --- a/elyra/pipeline/processor.py +++ b/elyra/pipeline/processor.py @@ -26,14 +26,10 @@ from typing import Dict from typing import List from typing import Optional -from typing import Set from typing import Union -import entrypoints from minio.error import S3Error from traitlets.config import Bool -from traitlets.config import default -from traitlets.config import List as ListTrait from traitlets.config import LoggingConfigurable from traitlets.config import SingletonConfigurable from traitlets.config import Unicode @@ -53,6 +49,7 @@ from elyra.pipeline.properties import KubernetesSecret from elyra.pipeline.properties import KubernetesToleration from elyra.pipeline.properties import VolumeMount +from elyra.pipeline.registry import PipelineProcessorRegistry from elyra.pipeline.runtime_type import RuntimeProcessorType from elyra.pipeline.runtime_type import RuntimeTypeResources from elyra.util.archive import create_temp_archive @@ -62,103 +59,6 @@ elyra_log_pipeline_info = os.getenv("ELYRA_LOG_PIPELINE_INFO", True) -class PipelineProcessorRegistry(SingletonConfigurable): - _processors: Dict[str, PipelineProcessor] - - # Runtimes - runtimes_env = "ELYRA_PROCESSOR_RUNTIMES" - runtimes = ListTrait( - default_value=None, - config=True, - allow_none=True, - help="""The runtimes to use during this Elyra instance. (env ELYRA_PROCESSOR_RUNTIMES)""", - ) - - @default("runtimes") - def _runtimes_default(self) -> Optional[List[str]]: - env_value = os.getenv(self.runtimes_env) - if env_value: - return env_value.replace(" ", "").split(",") - return None - - def __init__(self, **kwargs): - super().__init__(**kwargs) - root_dir: Optional[str] = kwargs.pop("root_dir", None) - self.root_dir = get_expanded_path(root_dir) - self._processors = {} - # Register all known processors based on entrypoint configuration - for processor in entrypoints.get_group_all("elyra.pipeline.processors"): - try: - # instantiate an actual instance of the processor - processor_instance = processor.load()(root_dir=self.root_dir, parent=kwargs.get("parent")) - if not self.runtimes or processor.name in self.runtimes: - self._add_processor(processor_instance) - else: - self.log.info( - f"Although runtime '{processor.name}' is installed, it is not in the set of " - f"runtimes configured via '--PipelineProcessorRegistry.runtimes' and will not " - f"be available." - ) - except Exception as err: - # log and ignore initialization errors - self.log.error( - f"Error registering {processor.name} processor " - f'"{processor.module_name}.{processor.object_name}" - {err}' - ) - - def _add_processor(self, processor_instance): - self.log.info( - f"Registering {processor_instance.name} processor " - f"'{processor_instance.__class__.__module__}.{processor_instance.__class__.__name__}'..." - ) - self._processors[processor_instance.name] = processor_instance - - def get_processor(self, processor_name: str): - if self.is_valid_processor(processor_name): - return self._processors[processor_name] - else: - raise RuntimeError(f"Could not find pipeline processor '{processor_name}'") - - def get_all_processors(self) -> List[PipelineProcessor]: - return list(self._processors.values()) - - def is_valid_processor(self, processor_name: str) -> bool: - return processor_name in self._processors.keys() - - def is_valid_runtime_type(self, runtime_type_name: str) -> bool: - for processor in self._processors.values(): - if processor.type.name == runtime_type_name.upper(): - return True - return False - - def get_runtime_types_resources(self) -> List[RuntimeTypeResources]: - """Returns the set of resource instances for each active runtime type""" - - # Build set of active runtime types, then build list of resources instances - runtime_types: Set[RuntimeProcessorType] = set() - enabled_runtimes: Set[RuntimeProcessorType] = set() # Track which runtimes are enabled - for name, processor in self._processors.items(): - runtime_types.add(processor.type) - enabled_runtimes.add(processor.type) - - # Unconditionally include "generic" resources since, to this point, all non-local runtimes - # also support generic components, so we need their resources on the frontend, despite the - # fact that the "local runtime" may not be enabled. When it is not enabled, it won't be in - # the runtime_types list, so, in that case, we add it, but not mark it as an enabled runtime. - if RuntimeProcessorType.LOCAL not in runtime_types: - runtime_types.add(RuntimeProcessorType.LOCAL) - - resources: List[RuntimeTypeResources] = list() - for runtime_type in runtime_types: - resources.append( - RuntimeTypeResources.get_instance_by_type( - runtime_type, runtime_enabled=(runtime_type in enabled_runtimes) - ) - ) - - return resources - - class PipelineProcessorManager(SingletonConfigurable): _registry: PipelineProcessorRegistry diff --git a/elyra/pipeline/registry.py b/elyra/pipeline/registry.py new file mode 100644 index 000000000..1a150e4bc --- /dev/null +++ b/elyra/pipeline/registry.py @@ -0,0 +1,126 @@ +# +# Copyright 2018-2022 Elyra Authors +# +# Licensed 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 os +from typing import Dict +from typing import List +from typing import Optional +from typing import Set + +import entrypoints +from traitlets.config import default +from traitlets.config import List as ListTrait +from traitlets.config import SingletonConfigurable + +from elyra.pipeline.runtime_type import RuntimeProcessorType +from elyra.pipeline.runtime_type import RuntimeTypeResources +from elyra.util.path import get_expanded_path + + +class PipelineProcessorRegistry(SingletonConfigurable): + _processors: Dict[str, object] # Map processor name to pipeline processor instance + + # Runtimes + runtimes_env = "ELYRA_PROCESSOR_RUNTIMES" + runtimes = ListTrait( + default_value=None, + config=True, + allow_none=True, + help="""The runtimes to use during this Elyra instance. (env ELYRA_PROCESSOR_RUNTIMES)""", + ) + + @default("runtimes") + def _runtimes_default(self) -> Optional[List[str]]: + env_value = os.getenv(self.runtimes_env) + if env_value: + return env_value.replace(" ", "").split(",") + return None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + root_dir: Optional[str] = kwargs.pop("root_dir", None) + self.root_dir = get_expanded_path(root_dir) + self._processors = {} + # Register all known processors based on entrypoint configuration + for processor in entrypoints.get_group_all("elyra.pipeline.processors"): + try: + # instantiate an actual instance of the processor + processor_instance = processor.load()(root_dir=self.root_dir, parent=kwargs.get("parent")) + if not self.runtimes or processor.name in self.runtimes: + self._add_processor(processor_instance) + else: + self.log.info( + f"Although runtime '{processor.name}' is installed, it is not in the set of " + f"runtimes configured via '--PipelineProcessorRegistry.runtimes' and will not " + f"be available." + ) + except Exception as err: + # log and ignore initialization errors + self.log.error( + f"Error registering {processor.name} processor " + f'"{processor.module_name}.{processor.object_name}" - {err}' + ) + + def _add_processor(self, processor_instance): + self.log.info( + f"Registering {processor_instance.name} processor " + f"'{processor_instance.__class__.__module__}.{processor_instance.__class__.__name__}'..." + ) + self._processors[processor_instance.name] = processor_instance + + def get_processor(self, processor_name: str): + if self.is_valid_processor(processor_name): + return self._processors[processor_name] + else: + raise RuntimeError(f"Could not find pipeline processor '{processor_name}'") + + def get_all_processors(self) -> List: + return list(self._processors.values()) + + def is_valid_processor(self, processor_name: str) -> bool: + return processor_name in self._processors.keys() + + def is_valid_runtime_type(self, runtime_type_name: str) -> bool: + for processor in self._processors.values(): + if processor.type.name == runtime_type_name.upper(): + return True + return False + + def get_runtime_types_resources(self) -> List[RuntimeTypeResources]: + """Returns the set of resource instances for each active runtime type""" + + # Build set of active runtime types, then build list of resources instances + runtime_types: Set[RuntimeProcessorType] = set() + enabled_runtimes: Set[RuntimeProcessorType] = set() # Track which runtimes are enabled + for name, processor in self._processors.items(): + runtime_types.add(processor.type) + enabled_runtimes.add(processor.type) + + # Unconditionally include "generic" resources since, to this point, all non-local runtimes + # also support generic components, so we need their resources on the frontend, despite the + # fact that the "local runtime" may not be enabled. When it is not enabled, it won't be in + # the runtime_types list, so, in that case, we add it, but not mark it as an enabled runtime. + if RuntimeProcessorType.LOCAL not in runtime_types: + runtime_types.add(RuntimeProcessorType.LOCAL) + + resources: List[RuntimeTypeResources] = list() + for runtime_type in runtime_types: + resources.append( + RuntimeTypeResources.get_instance_by_type( + runtime_type, runtime_enabled=(runtime_type in enabled_runtimes) + ) + ) + + return resources diff --git a/elyra/tests/pipeline/test_processor.py b/elyra/tests/pipeline/test_processor.py index cf4aeff6a..5a7b35315 100644 --- a/elyra/tests/pipeline/test_processor.py +++ b/elyra/tests/pipeline/test_processor.py @@ -20,8 +20,8 @@ from elyra.pipeline.kfp.processor_kfp import KfpPipelineProcessor from elyra.pipeline.pipeline import GenericOperation -from elyra.pipeline.processor import PipelineProcessorRegistry from elyra.pipeline.properties import ElyraProperty +from elyra.pipeline.registry import PipelineProcessorRegistry # --------------------------------------------------- From cf6d104831935a631ddab3a70090eb549bc61bfc Mon Sep 17 00:00:00 2001 From: Kevin Bates Date: Tue, 28 Feb 2023 16:28:05 -0800 Subject: [PATCH 23/25] Display configured runtimes in log indicating why a builtin runtime is not available Signed-off-by: Kevin Bates --- elyra/pipeline/registry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/elyra/pipeline/registry.py b/elyra/pipeline/registry.py index 1a150e4bc..8c519ebdc 100644 --- a/elyra/pipeline/registry.py +++ b/elyra/pipeline/registry.py @@ -63,8 +63,7 @@ def __init__(self, **kwargs): else: self.log.info( f"Although runtime '{processor.name}' is installed, it is not in the set of " - f"runtimes configured via '--PipelineProcessorRegistry.runtimes' and will not " - f"be available." + f"configured runtimes {self.runtimes} and will not be available." ) except Exception as err: # log and ignore initialization errors From 06cb604466c2cebab6d256374fa13fd4925978a2 Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Thu, 2 Mar 2023 11:28:54 -0600 Subject: [PATCH 24/25] Fix extra launcher tile for generic Signed-off-by: Martha Cryan --- packages/pipeline-editor/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/pipeline-editor/src/index.ts b/packages/pipeline-editor/src/index.ts index 44f944330..3fec06d3f 100644 --- a/packages/pipeline-editor/src/index.ts +++ b/packages/pipeline-editor/src/index.ts @@ -280,7 +280,8 @@ const extension: JupyterFrontEndPlugin = { PipelineService.getRuntimeTypes() .then(async types => { - const promises = types.map(async t => { + const filteredTypes = types.filter(t => t.runtime_enabled); + const promises = filteredTypes.map(async t => { return { ...t, icon: await createRemoteIcon({ From c1e1a80678f535f2d66e1f62b8965f255031696a Mon Sep 17 00:00:00 2001 From: Martha Cryan Date: Wed, 8 Mar 2023 12:03:50 -0600 Subject: [PATCH 25/25] Fix title context bug Signed-off-by: Martha Cryan --- packages/metadata-common/src/MetadataWidget.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/metadata-common/src/MetadataWidget.tsx b/packages/metadata-common/src/MetadataWidget.tsx index e91e94d8a..d767b1c58 100644 --- a/packages/metadata-common/src/MetadataWidget.tsx +++ b/packages/metadata-common/src/MetadataWidget.tsx @@ -403,11 +403,12 @@ export class MetadataWidget extends ReactWidget { } } - addMetadata(schema: string): void { + addMetadata(schema: string, titleContext?: string): void { this.openMetadataEditor({ onSave: this.updateMetadata, schemaspace: this.props.schemaspace, - schema: schema + schema: schema, + titleContext }); } @@ -521,7 +522,11 @@ export class MetadataWidget extends ReactWidget { className={`${METADATA_HEADER_BUTTON_CLASS} elyra-metadataHeader-addButton`} onClick={ singleSchema - ? (): void => this.addMetadata(this.schemas?.[0].name) + ? (): void => + this.addMetadata( + this.schemas?.[0].name, + this.titleContext + ) : (event: any): void => { this.props.app.contextMenu.open(event); }