Repository: GoogleCloudDataproc/dataproc-jupyter-plugin (language: python)

Issue:
Bump ip from 2.0.0 to 2.0.1 in /ui-tests
Bumps [ip](https://github.com/indutny/node-ip) from 2.0.0 to 2.0.1.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/indutny/node-ip/commit/3b0994a74eca51df01f08c40d6a65ba0e1845d04"><code>3b0994a</code></a> 2.0.1</li>
<li><a href="https://github.com/indutny/node-ip/commit/32f468f1245574785ec080705737a579be1223aa"><code>32f468f</code></a> lib: fixed CVE-2023-42282 and added unit test</li>
<li>See full diff in <a href="https://github.com/indutny/node-ip/compare/v2.0.0...v2.0.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ip&package-manager=npm_and_yarn&previous-version=2.0.0&new-version=2.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/GoogleCloudDataproc/dataproc-jupyter-plugin/network/alerts).

</details>


Discussion / hints:

Fix patch:
diff --git a/dataproc_jupyter_plugin/__init__.py b/dataproc_jupyter_plugin/__init__.py
index 6d4e942a..132eeb83 100644
--- a/dataproc_jupyter_plugin/__init__.py
+++ b/dataproc_jupyter_plugin/__init__.py
@@ -25,21 +25,21 @@
 
 from .handlers import setup_handlers, update_gateway_client_url
 
+
 class DataprocPluginConfig(Configurable):
-    log_path = Unicode("", config=True, help="File to log ServerApp and Dataproc Jupyter Plugin events.")
+    log_path = Unicode(
+        "",
+        config=True,
+        help="File to log ServerApp and Dataproc Jupyter Plugin events.",
+    )
 
 
 def _jupyter_labextension_paths():
-    return [{
-        "src": "labextension",
-        "dest": "dataproc_jupyter_plugin"
-    }]
+    return [{"src": "labextension", "dest": "dataproc_jupyter_plugin"}]
 
 
 def _jupyter_server_extension_points():
-    return [{
-        "module": "dataproc_jupyter_plugin"
-    }]
+    return [{"module": "dataproc_jupyter_plugin"}]
 
 
 def _link_jupyter_server_extension(server_app):
@@ -47,15 +47,15 @@ def _link_jupyter_server_extension(server_app):
     c.ServerApp.kernel_spec_manager_class = MixingKernelSpecManager
     c.ServerApp.kernel_manager_class = MixingMappingKernelManager
     c.ServerApp.session_manager_class = SessionManager
-    c.ServerApp.kernel_websocket_connection_class = (
-        DelegatingWebsocketConnection)
-    c.DelegatingWebsocketConnection.kernel_ws_protocol=""
+    c.ServerApp.kernel_websocket_connection_class = DelegatingWebsocketConnection
+    c.DelegatingWebsocketConnection.kernel_ws_protocol = ""
 
-    c.GatewayClient.auth_scheme = 'Bearer'
+    c.GatewayClient.auth_scheme = "Bearer"
     c.GatewayClient.headers = '{"Cookie": "_xsrf=XSRF", "X-XSRFToken": "XSRF"}'
     c.GatewayClient.gateway_token_renewer_class = CommandTokenRenewer
     c.CommandTokenRenewer.token_command = (
-        'gcloud config config-helper --format="value(credential.access_token)"')
+        'gcloud config config-helper --format="value(credential.access_token)"'
+    )
 
     # Version 2.8.0 of the `jupyter_server` package requires the `auth_token`
     # value to be set to a non-empty value or else it will never invoke the
@@ -64,16 +64,21 @@ def _link_jupyter_server_extension(server_app):
     #
     # See https://github.com/jupyter-server/jupyter_server/issues/1339 for more
     # details and discussion.
-    c.GatewayClient.auth_token = 'Initial, invalid value'
+    c.GatewayClient.auth_token = "Initial, invalid value"
 
     update_gateway_client_url(c, server_app.log)
 
     plugin_config = DataprocPluginConfig(config=server_app.config)
-    if (plugin_config.log_path != ""):
-        file_handler = logging.handlers.RotatingFileHandler(plugin_config.log_path, maxBytes= 2 * 1024 * 1024, backupCount=5)
-        file_handler.setFormatter(logging.Formatter('[%(levelname)s %(asctime)s %(name)s] %(message)s'))
+    if plugin_config.log_path != "":
+        file_handler = logging.handlers.RotatingFileHandler(
+            plugin_config.log_path, maxBytes=2 * 1024 * 1024, backupCount=5
+        )
+        file_handler.setFormatter(
+            logging.Formatter("[%(levelname)s %(asctime)s %(name)s] %(message)s")
+        )
         server_app.log.addHandler(file_handler)
 
+
 def _load_jupyter_server_extension(server_app):
     """Registers the API handler to receive HTTP requests from the frontend extension.
 
@@ -85,5 +90,7 @@ def _load_jupyter_server_extension(server_app):
     setup_handlers(server_app.web_app)
     name = "dataproc_jupyter_plugin"
     server_app.log.info(f"Registered {name} server extension")
+
+
 # For backward compatibility with notebook server - useful for Binder/JupyterHub
 load_jupyter_server_extension = _load_jupyter_server_extension
diff --git a/dataproc_jupyter_plugin/contollers/clusterController.py b/dataproc_jupyter_plugin/contollers/clusterController.py
new file mode 100644
index 00000000..27d7d583
--- /dev/null
+++ b/dataproc_jupyter_plugin/contollers/clusterController.py
@@ -0,0 +1,37 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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 json
+from jupyter_server.base.handlers import APIHandler
+import tornado
+from dataproc_jupyter_plugin import handlers
+from dataproc_jupyter_plugin.services.clusterListService import ClusterListService
+
+
+class ClusterListController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        try:
+            page_token = self.get_argument("pageToken")
+            page_size = self.get_argument("pageSize")
+            cluster = ClusterListService()
+            credentials = handlers.get_cached_credentials(self.log)
+            cluster_list = cluster.list_clusters(
+                credentials, page_size, page_token, self.log
+            )
+            self.finish(json.dumps(cluster_list))
+        except Exception as e:
+            self.log.exception(f"Error fetching cluster list")
+            self.finish({"error": str(e)})
diff --git a/dataproc_jupyter_plugin/contollers/composerController.py b/dataproc_jupyter_plugin/contollers/composerController.py
new file mode 100644
index 00000000..0c394a8d
--- /dev/null
+++ b/dataproc_jupyter_plugin/contollers/composerController.py
@@ -0,0 +1,40 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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 json
+from jupyter_server.base.handlers import APIHandler
+import tornado
+from dataproc_jupyter_plugin import handlers
+from dataproc_jupyter_plugin.services.composerService import ComposerService
+from requests import HTTPError
+
+
+class ComposerListController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        """Returns names of available composer environments"""
+        try:
+            environments_manager = ComposerService()
+            credentials = handlers.get_cached_credentials(self.log)
+            environments = environments_manager.list_environments(credentials, self.log)
+
+        except Exception as e:
+            self.log.exception(f"Error fetching composer environments: {str(e)}")
+            self.finish({"error": str(e)})
+
+        response = []
+        for environment in environments:
+            env = environment.dict()
+            response.append(env)
+        self.finish(json.dumps(response))
diff --git a/dataproc_jupyter_plugin/contollers/dagController.py b/dataproc_jupyter_plugin/contollers/dagController.py
new file mode 100644
index 00000000..a989e267
--- /dev/null
+++ b/dataproc_jupyter_plugin/contollers/dagController.py
@@ -0,0 +1,103 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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 json
+import subprocess
+from jupyter_server.base.handlers import APIHandler
+import tornado
+from dataproc_jupyter_plugin import handlers
+from dataproc_jupyter_plugin.services.dagListService import (
+    DagListService,
+    DagDeleteService,
+    DagUpdateService,
+)
+from dataproc_jupyter_plugin.utils.constants import TAGS
+
+
+class DagListController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        try:
+            dag = DagListService()
+            composer_name = self.get_argument("composer")
+            credentials = handlers.get_cached_credentials(self.log)
+            dag_list = dag.list_jobs(credentials, composer_name, TAGS, self.log)
+            self.finish(json.dumps(dag_list))
+        except Exception as e:
+            self.log.exception(f"Error fetching cluster list")
+            self.finish({"error": str(e)})
+            self.finish({"error": str(e)})
+
+
+class DagDownloadController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        try:
+            dag_id = self.get_argument("dag_id")
+            bucket_name = self.get_argument("bucket_name")
+            cmd = f"gsutil cp 'gs://{bucket_name}/dataproc-notebooks/{dag_id}/input_notebooks/*' ~/Downloads"
+            process = subprocess.Popen(
+                cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
+            )
+            output, _ = process.communicate()
+            if process.returncode == 0:
+                self.finish({"status": 0})
+            else:
+                self.log.exception(f"Error downloading input notebook")
+                self.finish({"status": 1})
+        except Exception as e:
+            self.log.exception(f"Error downloading input notebook: {str(e)}")
+            self.finish({"error": str(e)})
+
+
+class DagDeleteController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        try:
+            dag = DagDeleteService()
+            composer = self.get_argument("composer")
+            dag_id = self.get_argument("dag_id")
+            credentials = handlers.get_cached_credentials(self.log)
+            delete_response = dag.delete_job(credentials, composer, dag_id, self.log)
+            if delete_response == 0:
+                self.finish({"status": 0})
+            else:
+                self.log.exception(f"Error deleting input notebook")
+                self.finish(json.dumps(delete_response))
+        except Exception as e:
+            self.log.exception(f"Error deleting input notebook: {str(e)}")
+            self.finish({"error": str(e)})
+
+
+class DagUpdateController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        try:
+            dag = DagUpdateService()
+            composer = self.get_argument("composer")
+            dag_id = self.get_argument("dag_id")
+            status = self.get_argument("status")
+            credentials = handlers.get_cached_credentials(self.log)
+            update_response = dag.update_job(
+                credentials, composer, dag_id, status, self.log
+            )
+            if update_response == 0:
+                self.finish({"status": 0})
+            else:
+                self.log.exception(f"Error updating status")
+                self.finish(json.dumps(update_response))
+        except Exception as e:
+            self.log.exception(f"Error updating status: {str(e)}")
+            self.finish({"error": str(e)})
diff --git a/dataproc_jupyter_plugin/contollers/dagRunController.py b/dataproc_jupyter_plugin/contollers/dagRunController.py
new file mode 100644
index 00000000..f3f22d91
--- /dev/null
+++ b/dataproc_jupyter_plugin/contollers/dagRunController.py
@@ -0,0 +1,87 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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 json
+from jupyter_server.base.handlers import APIHandler
+import tornado
+from dataproc_jupyter_plugin import handlers
+from dataproc_jupyter_plugin.services.dagRunService import (
+    DagRunListService,
+    DagRunTaskListService,
+    DagRunTaskLogsListService,
+)
+
+
+class DagRunController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        try:
+            dag_run = DagRunListService()
+            composer_name = self.get_argument("composer")
+            dag_id = self.get_argument("dag_id")
+            start_date = self.get_argument("start_date")
+            end_date = self.get_argument("end_date")
+            credentials = handlers.get_cached_credentials(self.log)
+            dag_run_list = dag_run.list_dag_runs(
+                credentials, composer_name, dag_id, start_date, end_date, self.log
+            )
+            self.finish(json.dumps(dag_run_list))
+        except Exception as e:
+            self.log.exception(f"Error fetching dag run list {str(e)}")
+            self.finish({"error": str(e)})
+
+
+class DagRunTaskController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        try:
+            dag_run = DagRunTaskListService()
+            composer_name = self.get_argument("composer")
+            dag_id = self.get_argument("dag_id")
+            dag_run_id = self.get_argument("dag_run_id")
+            credentials = handlers.get_cached_credentials(self.log)
+            dag_run_list = dag_run.list_dag_run_task(
+                credentials, composer_name, dag_id, dag_run_id, self.log
+            )
+            self.finish(json.dumps(dag_run_list))
+        except Exception as e:
+            self.log.exception(f"Error fetching dag run tasks: {str(e)}")
+            self.finish({"error": str(e)})
+
+
+class DagRunTaskLogsController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        try:
+            dag_run = DagRunTaskLogsListService()
+            composer_name = self.get_argument("composer")
+            dag_id = self.get_argument("dag_id")
+            dag_run_id = self.get_argument("dag_run_id")
+            task_id = self.get_argument("task_id")
+            task_try_number = self.get_argument("task_try_number")
+            credentials = handlers.get_cached_credentials(self.log)
+            dag_run_list = dag_run.list_dag_run_task_logs(
+                credentials,
+                composer_name,
+                dag_id,
+                dag_run_id,
+                task_id,
+                task_try_number,
+                self.log,
+            )
+            self.finish(json.dumps(dag_run_list))
+        except Exception as e:
+            self.log.exception(f"Error fetching dag run task logs: {str(e)}")
+            self.finish({"error": str(e)})
diff --git a/dataproc_jupyter_plugin/contollers/editDagController.py b/dataproc_jupyter_plugin/contollers/editDagController.py
new file mode 100644
index 00000000..46899197
--- /dev/null
+++ b/dataproc_jupyter_plugin/contollers/editDagController.py
@@ -0,0 +1,35 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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 json
+from jupyter_server.base.handlers import APIHandler
+import tornado
+from dataproc_jupyter_plugin import handlers
+from dataproc_jupyter_plugin.services.editDagService import DagEditService
+from dataproc_jupyter_plugin.utils.constants import TAGS
+
+
+class EditDagController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        try:
+            dag = DagEditService()
+            bucket_name = self.get_argument("bucket_name")
+            dag_id = self.get_argument("dag_id")
+            credentials = handlers.get_cached_credentials(self.log)
+            dag_details = dag.edit_jobs(dag_id, bucket_name, credentials, self.log)
+            self.finish(json.dumps(dag_details))
+        except Exception as e:
+            self.log.exception(f"Error getting dag details")
+            self.finish({"error": str(e)})
diff --git a/dataproc_jupyter_plugin/contollers/executorController.py b/dataproc_jupyter_plugin/contollers/executorController.py
new file mode 100644
index 00000000..1d319a55
--- /dev/null
+++ b/dataproc_jupyter_plugin/contollers/executorController.py
@@ -0,0 +1,32 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+
+
+from jupyter_server.base.handlers import APIHandler
+import tornado
+from dataproc_jupyter_plugin import handlers
+from dataproc_jupyter_plugin.services.executorService import ExecutorService
+
+
+class ExecutorController(APIHandler):
+    @tornado.web.authenticated
+    def post(self):
+        try:
+            input_data = self.get_json_body()
+            execute = ExecutorService()
+            credentials = handlers.get_cached_credentials(self.log)
+            execute.execute(credentials, input_data, self.log)
+        except Exception as e:
+            self.log.exception(f"Error creating dag schedule: {str(e)}")
+            self.finish({"error": str(e)})
diff --git a/dataproc_jupyter_plugin/contollers/runtimeController.py b/dataproc_jupyter_plugin/contollers/runtimeController.py
new file mode 100644
index 00000000..ea153369
--- /dev/null
+++ b/dataproc_jupyter_plugin/contollers/runtimeController.py
@@ -0,0 +1,37 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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 json
+from jupyter_server.base.handlers import APIHandler
+import tornado
+from dataproc_jupyter_plugin import handlers
+from dataproc_jupyter_plugin.services.runtimeListService import RuntimeListService
+
+
+class RuntimeController(APIHandler):
+    @tornado.web.authenticated
+    def get(self):
+        try:
+            page_token = self.get_argument("pageToken")
+            page_size = self.get_argument("pageSize")
+            runtime = RuntimeListService()
+            credentials = handlers.get_cached_credentials(self.log)
+            runtime_list = runtime.list_runtime(
+                credentials, page_size, page_token, self.log
+            )
+            self.finish(json.dumps(runtime_list))
+        except Exception as e:
+            self.log.exception(f"Error fetching runtime template list: {str(e)}")
+            self.finish({"error": str(e)})
diff --git a/dataproc_jupyter_plugin/dagTemplates/pysparkBatchTemplate-v1.txt b/dataproc_jupyter_plugin/dagTemplates/pysparkBatchTemplate-v1.txt
new file mode 100644
index 00000000..4018b1ec
--- /dev/null
+++ b/dataproc_jupyter_plugin/dagTemplates/pysparkBatchTemplate-v1.txt
@@ -0,0 +1,83 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+
+
+from datetime import datetime, timedelta, timezone
+import uuid
+from airflow import DAG
+from airflow.providers.google.cloud.operators.dataproc import DataprocCreateBatchOperator
+
+
+default_args = {
+    'owner': '{{owner}}',
+    'start_date': '{{start_date}}',
+    'retries': '{{retry_count}}',
+    'retry_delay': timedelta(minutes=int('{{retry_delay}}')), 
+    'email': {{email}}, 
+    'email_on_failure': '{{email_failure}}',     
+    'email_on_retry': '{{email_delay}}',      
+    'email_on_success':'{{email_success}}'
+}
+time_zone = '{{time_zone}}'
+serverless_name = '{{serverless_name}}'
+input_notebook = '{{input_notebook}}'
+output_notebook = '{{output_notebook}}'
+parameters = '''
+{{parameters}}
+'''
+notebook_args= [input_notebook, output_notebook] 
+# Check if parameters is not empty or contains only whitespace
+if parameters.strip(): 
+    notebook_args.extend(["--parameters", parameters])
+
+
+dag = DAG(
+    '{{name}}', 
+    default_args=default_args,
+    description='{{name}}',
+    tags =['dataproc_jupyter_plugin'],
+    schedule_interval='{{schedule_interval}}',
+    catchup = False
+)
+
+
+create_batch = DataprocCreateBatchOperator(
+        task_id="batch_create",
+        project_id = '{{gcpProjectId}}',
+        region = '{{gcpRegion}}',
+        batch={
+            "pyspark_batch": {
+                "main_python_file_uri": '{{inputFilePath}}',
+                "args": notebook_args                
+            },
+            "environment_config": {
+                "peripherals_config": {
+                    {% if metastore_service %}
+                    "metastore_service": '{{metastore_service}}',
+                    {% endif %}
+                    "spark_history_server_config": {
+                        "dataproc_cluster": '{{phs_path}}',
+                    },
+                },
+            },
+            {% if custom_container %}
+            "runtime_config": {
+                "container_image": '{{custom_container}}'
+            },
+            {% endif %}
+        },
+        batch_id=str(uuid.uuid4()),
+        dag = dag,
+    )
+
diff --git a/dataproc_jupyter_plugin/dagTemplates/pysparkJobTemplate-v1.txt b/dataproc_jupyter_plugin/dagTemplates/pysparkJobTemplate-v1.txt
new file mode 100644
index 00000000..65da47e8
--- /dev/null
+++ b/dataproc_jupyter_plugin/dagTemplates/pysparkJobTemplate-v1.txt
@@ -0,0 +1,152 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+
+from datetime import datetime, timedelta, timezone
+from airflow import DAG
+from airflow.providers.google.cloud.operators.dataproc import DataprocSubmitJobOperator
+from airflow.operators.python_operator import PythonOperator
+from google.cloud import dataproc_v1
+from google.api_core.client_options import ClientOptions
+
+default_args = {
+    'owner': '{{owner}}',
+    'start_date': '{{start_date}}',
+    'retries': '{{retry_count}}',
+    'retry_delay': timedelta(minutes=int('{{retry_delay}}')), 
+    'email': {{email}}, 
+    'email_on_failure': '{{email_failure}}',     
+    'email_on_retry': '{{email_delay}}',     
+    'email_on_success':'{{email_success}}'
+}
+time_zone = '{{time_zone}}'
+stop_cluster_check = '{{stop_cluster}}'
+input_notebook = '{{input_notebook}}'
+output_notebook = '{{output_notebook}}'
+parameters = '''
+{{parameters}}
+'''
+notebook_args= [input_notebook, output_notebook] 
+# Check if parameters is not empty or contains only whitespace
+if parameters.strip():  
+    notebook_args.extend(["--parameters", parameters])
+
+def get_client_cert():
+    # code to load client certificate and private key.
+    return client_cert_bytes, client_private_key_bytes
+ 
+
+def get_cluster_state_start_if_not_running():
+
+    options = ClientOptions(api_endpoint="{{gcpRegion}}-dataproc.googleapis.com:443",
+    client_cert_source=get_client_cert)
+
+    # Create a client
+    client = dataproc_v1.ClusterControllerClient(client_options=options)
+
+    # Initialize request argument(s)
+    request = dataproc_v1.GetClusterRequest(
+        project_id='{{gcpProjectId}}',
+        region='{{gcpRegion}}',
+        cluster_name='{{cluster_name}}',
+    )
+
+    # Make the request
+    response = client.get_cluster(request=request)    
+   
+    # Handle the response
+    print(f"State is {response.status.state}")
+    if response.status.state in (6, 7):
+        print("Cluster is in stopped/stopping state. Starting the cluster")
+        request1 = dataproc_v1.StartClusterRequest(
+            project_id='{{gcpProjectId}}',
+            region='{{gcpRegion}}',
+            cluster_name='{{cluster_name}}',
+        )
+        operation = client.start_cluster(request=request1)
+        print("Waiting for operation to complete...")
+        response = operation.result()
+        if response.status.state in (2, 5):
+            print("Cluster is started succesfully")    
+    elif response.status.state in (2, 5):
+       print("Cluster is already running")
+    else:
+        print("Cluster is unavailable")
+        raise Exception("Cluster is unavailable")
+
+ 
+def stop_the_cluster():
+    if '{{stop_cluster}}' == 'True':
+        options = ClientOptions(api_endpoint="{{gcpRegion}}-dataproc.googleapis.com:443",
+            client_cert_source=get_client_cert)
+    
+        # Create a client
+        client = dataproc_v1.ClusterControllerClient(client_options=options)
+    
+        # Initialize request argument(s)
+        request = dataproc_v1.StopClusterRequest(
+            project_id='{{gcpProjectId}}',
+            region='{{gcpRegion}}',
+            cluster_name='{{cluster_name}}',
+        )
+    
+        # Make the request
+        operation = client.stop_cluster(request=request)
+        print("Waiting for operation to complete...")
+        response = operation.result()
+        if response.status.state in (6, 7):
+            print("Cluster is stopped succesfully")
+    
+        # Handle the response
+        print(response)
+
+dag = DAG(
+    '{{name}}', 
+    default_args=default_args,
+    description='{{name}}',
+    tags =['dataproc_jupyter_plugin'],
+    schedule_interval='{{schedule_interval}}',
+    catchup= False
+)
+
+start_cluster = PythonOperator(
+    task_id='start_cluster',
+    python_callable=get_cluster_state_start_if_not_running,
+    retries= 2,
+    provide_context=True,
+    dag=dag)
+
+submit_pyspark_job = DataprocSubmitJobOperator(
+    task_id='submit_pyspark_job',
+    project_id='{{gcpProjectId}}',  # This parameter can be overridden by the connection
+    region='{{gcpRegion}}',  # This parameter can be overridden by the connection 
+    job={
+        'reference': {'project_id': '{{gcpProjectId}}'},
+        'placement': {'cluster_name': '{{cluster_name}}'},
+        'labels': {'client': 'dataproc-jupyter-plugin'},
+        'pyspark_job': {
+            'main_python_file_uri': '{{inputFilePath}}',
+            'args' : notebook_args
+        },
+    },
+    gcp_conn_id='google_cloud_default',  # Reference to the GCP connection
+    dag=dag,
+)
+
+stop_cluster = PythonOperator(
+        task_id='stop_cluster',
+        python_callable=stop_the_cluster,
+        provide_context=True,
+        dag=dag)
+    
+start_cluster >> submit_pyspark_job >> stop_cluster 
\ No newline at end of file
diff --git a/dataproc_jupyter_plugin/dagTemplates/wrapper_papermill.py b/dataproc_jupyter_plugin/dagTemplates/wrapper_papermill.py
new file mode 100644
index 00000000..315bdbff
--- /dev/null
+++ b/dataproc_jupyter_plugin/dagTemplates/wrapper_papermill.py
@@ -0,0 +1,103 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+
+
+r"""Wrapper script for executing notebooks using Dataproc jobs API.
+
+Usage: `gcloud dataproc jobs submit pyspark --cluster $CLUSTER_NAME wrapper.py \
+        -- gs://$INPUT_LOCATION gs://$OUTPUT_LOCATION` \
+        --parameters-file=gs://$PARAM_YAML_LOCATION \
+        --parameters='foo: bar'
+
+Only tested with Dataproc version 2.0+ images
+"""
+
+import argparse
+import re
+from google.cloud.storage.client import Client
+import papermill as pm
+import yaml
+import sys
+
+
+def main():
+    parser = argparse.ArgumentParser()
+
+    parser.add_argument(
+        "input",
+        metavar="input",
+        type=str,
+        help="GCS URI location of the input notebook",
+    )
+    parser.add_argument(
+        "output",
+        metavar="output",
+        type=str,
+        help="GCS URI location of the output executed notebook",
+    )
+    parser.add_argument(
+        "--parameters-file",
+        metavar="parameters_file",
+        help="GCS URI location of yaml file defining parameters values.",
+    )
+    parser.add_argument(
+        "--parameters",
+        metavar="parameters",
+        help="YAML string defining parameter values.",
+    )
+    args = parser.parse_args()
+
+    matched = re.match("gs://([^/]+)/(.+)", args.output)
+    if not matched:
+        raise ValueError('Invalid output URI: "%s"' % args.output)
+    
+    # Download input notebook and params file from GCS
+    # Note that papermill supports gcsfs by default, but some type of dependency
+    # problem appears to prevent this from working out of the box on 2.0 images
+    gcs = Client()
+
+    params = {}
+    if args.parameters_file:
+        print('Reading parameter yaml from "%s"' % args.parameters_file)
+        with open("parameters.yaml", "wb+") as f:
+            gcs.download_blob_to_file(args.parameters_file, f)
+            f.seek(0)
+            params = yaml.safe_load(f)
+
+    if args.parameters:
+        params.update(yaml.safe_load(args.parameters))
+    print("Found parameters %s" % params)
+
+    print('Reading notebook from "%s"' % args.input)
+    with open("input.ipynb", "wb") as f:
+        gcs.download_blob_to_file(args.input, f)
+
+    pm.execute_notebook(
+        "input.ipynb",
+        "output.ipynb",
+        kernel_name="python3",
+        log_output=True,
+        progress_bar=False,
+        stdout_file=sys.stdout,
+        parameters=params
+    )
+
+    print('Writing result to "%s"' % args.output)
+    bucket = gcs.get_bucket(matched.group(1))
+    blob = bucket.blob(matched.group(2))
+    blob.upload_from_filename("output.ipynb")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/dataproc_jupyter_plugin/handlers.py b/dataproc_jupyter_plugin/handlers.py
index 489f9350..0ae3c20c 100644
--- a/dataproc_jupyter_plugin/handlers.py
+++ b/dataproc_jupyter_plugin/handlers.py
@@ -15,21 +15,43 @@
 import json
 from jupyter_server.base.handlers import APIHandler
 from jupyter_server.utils import url_path_join
+from requests import HTTPError
 import tornado
 import subprocess
 from cachetools import TTLCache
 import datetime
 import re
 import threading
+from jupyter_server.utils import ensure_async
+import time
+
 
 from google.cloud.jupyter_config.config import gcp_kernel_gateway_url, get_gcloud_config
+from dataproc_jupyter_plugin.contollers.clusterController import ClusterListController
+from dataproc_jupyter_plugin.contollers.composerController import ComposerListController
+from dataproc_jupyter_plugin.contollers.dagController import (
+    DagDeleteController,
+    DagDownloadController,
+    DagListController,
+    DagUpdateController,
+)
+from dataproc_jupyter_plugin.contollers.dagRunController import (
+    DagRunController,
+    DagRunTaskController,
+    DagRunTaskLogsController,
+)
+from dataproc_jupyter_plugin.contollers.editDagController import EditDagController
+from dataproc_jupyter_plugin.contollers.executorController import ExecutorController
+from dataproc_jupyter_plugin.contollers.runtimeController import RuntimeController
 
 
 def update_gateway_client_url(c, log):
     try:
         kernel_gateway_url = gcp_kernel_gateway_url()
     except subprocess.SubprocessError as e:
-        log.warning(f"Error constructing the kernel gateway URL; configure your project, region, and credentials using gcloud: {e}")
+        log.warning(
+            f"Error constructing the kernel gateway URL; configure your project, region, and credentials using gcloud: {e}"
+        )
         return
     log.info(f"Updating remote kernel gateway URL to {kernel_gateway_url}")
     c.GatewayClient.url = kernel_gateway_url
@@ -37,88 +59,118 @@ def update_gateway_client_url(c, log):
 
 credentials_cache = None
 
+
 def get_cached_credentials(log):
     global credentials_cache
-
     try:
-        cmd = "gcloud config config-helper --format=json"
-        process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
-        output, error = process.communicate()
-        if process.returncode == 0:
-            config_data = json.loads(output)
-            credentials = {
-                    'project_id': config_data['configuration']['properties']['core']['project'],
-                    'region_id': config_data['configuration']['properties']['compute']['region'],
-                    'access_token': config_data['credential']['access_token']
+        if credentials_cache is None or "credentials" not in credentials_cache:
+            cmd = "gcloud config config-helper --format=json"
+            process = subprocess.Popen(
+                cmd,
+                shell=True,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                text=True,
+            )
+            output, error = process.communicate()
+            if process.returncode == 0:
+                config_data = json.loads(output)
+                credentials = {
+                    "project_id": config_data["configuration"]["properties"]["core"][
+                        "project"
+                    ],
+                    "region_id": config_data["configuration"]["properties"]["compute"][
+                        "region"
+                    ],
+                    "access_token": config_data["credential"]["access_token"],
                 }
 
-            token_expiry = config_data['credential']['token_expiry']
-            utc_datetime = datetime.datetime.strptime(token_expiry, '%Y-%m-%dT%H:%M:%SZ')
-            current_utc_datetime = datetime.datetime.utcnow()
-            expiry_timedelta = utc_datetime - current_utc_datetime
-            expiry_seconds = expiry_timedelta.total_seconds()
-            if expiry_seconds > 1000:
-                ttl_seconds = 1000
-            else:
-                ttl_seconds = expiry_seconds
-            credentials_cache = TTLCache(maxsize=1, ttl=ttl_seconds)
-            credentials_cache['credentials'] = credentials
-            return credentials
-        else:
-            cmd = "gcloud config get-value account"
-            process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
-            output, error = process.communicate()
-            if output == '':
-                cmd = "gcloud config get-value project"
-                process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
-                project, error = process.communicate()
-                if project == '':
-                        credentials = {
-                    'project_id': '',
-                    'region_id': '',
-                    'access_token': '',
-                    'config_error': 1,
-                    'login_error': 0
-                    }
+                token_expiry = config_data["credential"]["token_expiry"]
+                utc_datetime = datetime.datetime.strptime(
+                    token_expiry, "%Y-%m-%dT%H:%M:%SZ"
+                )
+                current_utc_datetime = datetime.datetime.utcnow()
+                expiry_timedelta = utc_datetime - current_utc_datetime
+                expiry_seconds = expiry_timedelta.total_seconds()
+                if expiry_seconds > 1000:
+                    ttl_seconds = 1000
                 else:
-                    cmd = "gcloud config get-value compute/region"
-                    process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
-                    region, error = process.communicate()
-                    if region == '':
+                    ttl_seconds = expiry_seconds
+                credentials_cache = TTLCache(maxsize=1, ttl=ttl_seconds)
+                credentials_cache["credentials"] = credentials
+                return credentials
+            else:
+                cmd = "gcloud config get-value account"
+                process = subprocess.Popen(
+                    cmd,
+                    shell=True,
+                    stdout=subprocess.PIPE,
+                    stderr=subprocess.PIPE,
+                    text=True,
+                )
+                output, error = process.communicate()
+                if output == "":
+                    cmd = "gcloud config get-value project"
+                    process = subprocess.Popen(
+                        cmd,
+                        shell=True,
+                        stdout=subprocess.PIPE,
+                        stderr=subprocess.PIPE,
+                        text=True,
+                    )
+                    project, error = process.communicate()
+                    if project == "":
                         credentials = {
-                            'project_id': '',
-                            'region_id': '',
-                            'access_token': '',
-                            'config_error': 1,
-                            'login_error': 0
+                            "project_id": "",
+                            "region_id": "",
+                            "access_token": "",
+                            "config_error": 1,
+                            "login_error": 0,
                         }
                     else:
-                        credentials = {
-                            'project_id': '',
-                            'region_id': '',
-                            'access_token': '',
-                            'config_error': 0,
-                            'login_error': 1
+                        cmd = "gcloud config get-value compute/region"
+                        process = subprocess.Popen(
+                            cmd,
+                            shell=True,
+                            stdout=subprocess.PIPE,
+                            stderr=subprocess.PIPE,
+                            text=True,
+                        )
+                        region, error = process.communicate()
+                        if region == "":
+                            credentials = {
+                                "project_id": "",
+                                "region_id": "",
+                                "access_token": "",
+                                "config_error": 1,
+                                "login_error": 0,
                             }
-            else:
-                credentials = {
-                    'project_id': '',
-                    'region_id': '',
-                    'access_token': '',
-                    'config_error': 1,
-                    'login_error': 0
+                        else:
+                            credentials = {
+                                "project_id": "",
+                                "region_id": "",
+                                "access_token": "",
+                                "config_error": 0,
+                                "login_error": 1,
+                            }
+                else:
+                    credentials = {
+                        "project_id": "",
+                        "region_id": "",
+                        "access_token": "",
+                        "config_error": 1,
+                        "login_error": 0,
                     }
-            credentials_cache = TTLCache(maxsize=1, ttl=5)
-            credentials_cache['credentials'] = credentials
-            return credentials
+                credentials_cache = TTLCache(maxsize=1, ttl=5)
+                credentials_cache["credentials"] = credentials
+                return credentials
+        else:
+            return credentials_cache["credentials"]
     except Exception:
         log.exception(f"Error fetching credentials from gcloud")
-        credentials = {
-            'access_token': '',
-            'config_error': 1
-        }
+        credentials = {"access_token": "", "config_error": 1}
         credentials_cache = TTLCache(maxsize=1, ttl=2)
-        credentials_cache['credentials'] = credentials
+        credentials_cache["credentials"] = credentials
         return credentials
 
 
@@ -127,16 +179,17 @@ class RouteHandler(APIHandler):
     # patch, put, delete, options) to ensure only authorized user can request the
     # Jupyter server
     credentials_cache = None
+
     @tornado.web.authenticated
     def get(self):
         try:
-            if credentials_cache is None or 'credentials' not in credentials_cache:
+            if credentials_cache is None or "credentials" not in credentials_cache:
                 cached_credentials = get_cached_credentials(self.log)
                 self.finish(json.dumps(cached_credentials))
             else:
                 t1 = threading.Thread(target=get_cached_credentials, args=([self.log]))
                 t1.start()
-                self.finish(json.dumps(credentials_cache['credentials']))
+                self.finish(json.dumps(credentials_cache["credentials"]))
         except Exception:
             self.log.exception(f"Error handling credential request")
             cached_credentials = get_cached_credentials(self.log)
@@ -147,13 +200,15 @@ class LoginHandler(APIHandler):
     @tornado.web.authenticated
     def get(self):
         cmd = "gcloud auth login"
-        process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
+        process = subprocess.Popen(
+            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
+        )
         output, _ = process.communicate()
         # Check if the authentication was successful
         if process.returncode == 0:
-            self.finish({'login' : 'SUCCEEDED'})
+            self.finish({"login": "SUCCEEDED"})
         else:
-            self.finish({'login' : 'FAILED'})
+            self.finish({"login": "FAILED"})
 
 
 class ConfigHandler(APIHandler):
@@ -164,88 +219,140 @@ def post(self):
         input_data = self.get_json_body()
         project_id = input_data["projectId"]
         region = input_data["region"]
-        cmd = 'gcloud config set project '+project_id
-        project_set = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
+        cmd = "gcloud config set project " + project_id
+        project_set = subprocess.Popen(
+            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
+        )
         output, _ = project_set.communicate()
         if project_set.returncode == 0:
-            cmd = 'gcloud config set compute/region '+region
-            region_set = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
+            cmd = "gcloud config set compute/region " + region
+            region_set = subprocess.Popen(
+                cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
+            )
             output, _ = region_set.communicate()
             if region_set.returncode == 0:
                 credentials_cache = None
                 update_gateway_client_url(self.config, self.log)
-                self.finish({'config' : ERROR_MESSAGE + 'successful'})
+                self.finish({"config": ERROR_MESSAGE + "successful"})
             else:
-                self.finish({'config' : ERROR_MESSAGE + 'failed'})
+                self.finish({"config": ERROR_MESSAGE + "failed"})
         else:
-            self.finish({'config' : ERROR_MESSAGE + 'failed'})
+            self.finish({"config": ERROR_MESSAGE + "failed"})
 
 
 class UrlHandler(APIHandler):
+    url = {}
+
     @tornado.web.authenticated
     def get(self):
-        url = {}
-        dataproc_url = self.gcp_service_url('dataproc')
-        compute_url = self.gcp_service_url('compute', default_url='https://compute.googleapis.com/compute/v1')
-        metastore_url = self.gcp_service_url('metastore')
-        cloudkms_url = self.gcp_service_url('cloudkms')
-        cloudresourcemanager_url = self.gcp_service_url('cloudresourcemanager')
-        datacatalog_url = self.gcp_service_url('datacatalog')
-        storage_url = self.gcp_service_url('storage', default_url='https://storage.googleapis.com/storage/v1/')
+        dataproc_url = self.gcp_service_url("dataproc")
+        compute_url = self.gcp_service_url(
+            "compute", default_url="https://compute.googleapis.com/compute/v1"
+        )
+        metastore_url = self.gcp_service_url("metastore")
+        cloudkms_url = self.gcp_service_url("cloudkms")
+        cloudresourcemanager_url = self.gcp_service_url("cloudresourcemanager")
+        datacatalog_url = self.gcp_service_url("datacatalog")
+        storage_url = self.gcp_service_url(
+            "storage", default_url="https://storage.googleapis.com/storage/v1/"
+        )
         url = {
-            'dataproc_url': dataproc_url,
-            'compute_url': compute_url,
-            'metastore_url': metastore_url,
-            'cloudkms_url': cloudkms_url,
-            'cloudresourcemanager_url': cloudresourcemanager_url,
-            'datacatalog_url': datacatalog_url,
-            'storage_url': storage_url
-            }
+            "dataproc_url": dataproc_url,
+            "compute_url": compute_url,
+            "metastore_url": metastore_url,
+            "cloudkms_url": cloudkms_url,
+            "cloudresourcemanager_url": cloudresourcemanager_url,
+            "datacatalog_url": datacatalog_url,
+            "storage_url": storage_url,
+        }
         self.finish(url)
 
     def gcp_service_url(self, service_name, default_url=None):
-        default_url = default_url or f'https://{service_name}.googleapis.com/'
-        configured_url = get_gcloud_config(f'configuration.properties.api_endpoint_overrides.{service_name}')
+        default_url = default_url or f"https://{service_name}.googleapis.com/"
+        configured_url = get_gcloud_config(
+            f"configuration.properties.api_endpoint_overrides.{service_name}"
+        )
         url = configured_url or default_url
-        self.log.info(f'Service_url for service {service_name}: {url}')
+        self.log.info(f"Service_url for service {service_name}: {url}")
         return url
 
 
 class LogHandler(APIHandler):
     @tornado.web.authenticated
     def post(self):
-        logger = self.log.getChild('DataprocPluginClient')
+        logger = self.log.getChild("DataprocPluginClient")
         log_body = self.get_json_body()
         logger.log(log_body["level"], log_body["message"])
-        self.finish({'status': 'OK'})
+        self.finish({"status": "OK"})
 
 
 def setup_handlers(web_app):
     host_pattern = ".*$"
 
     base_url = web_app.settings["base_url"]
+    application_url = "dataproc-plugin"
 
     # Prepend the base_url so that it works in a JupyterHub setting
-    route_pattern = url_path_join(base_url, "dataproc-plugin", "credentials")
+    route_pattern = url_path_join(base_url, application_url, "credentials")
     handlers = [(route_pattern, RouteHandler)]
     web_app.add_handlers(host_pattern, handlers)
 
-    route_pattern = url_path_join(base_url, "dataproc-plugin", "login")
+    route_pattern = url_path_join(base_url, application_url, "login")
     handlers = [(route_pattern, LoginHandler)]
     web_app.add_handlers(host_pattern, handlers)
 
-    route_pattern = url_path_join(base_url, "dataproc-plugin", "configuration")
+    route_pattern = url_path_join(base_url, application_url, "configuration")
     handlers = [(route_pattern, ConfigHandler)]
     web_app.add_handlers(host_pattern, handlers)
 
-    route_pattern = url_path_join(base_url, "dataproc-plugin", "getGcpServiceUrls")
+    route_pattern = url_path_join(base_url, application_url, "getGcpServiceUrls")
     handlers = [(route_pattern, UrlHandler)]
     web_app.add_handlers(host_pattern, handlers)
 
-    route_pattern = url_path_join(base_url, "dataproc-plugin", "log")
+    route_pattern = url_path_join(base_url, application_url, "log")
     handlers = [(route_pattern, LogHandler)]
     web_app.add_handlers(host_pattern, handlers)
 
-    route_pattern = url_path_join(base_url, "dataproc-plugin", "getGcpServiceUrls")
-    handlers = [(route_pattern, UrlHandler)]
+    route_pattern = url_path_join(base_url, application_url, "composerList")
+    handlers = [(route_pattern, ComposerListController)]
+    web_app.add_handlers(host_pattern, handlers)
+
+    route_pattern = url_path_join(base_url, application_url, "dagRun")
+    route_pattern_dag_task = url_path_join(base_url, application_url, "dagRunTask")
+    route_pattern_dag_task_logs = url_path_join(
+        base_url, application_url, "dagRunTaskLogs"
+    )
+    handlers = [
+        (route_pattern, DagRunController),
+        (route_pattern_dag_task, DagRunTaskController),
+        (route_pattern_dag_task_logs, DagRunTaskLogsController),
+    ]
+    web_app.add_handlers(host_pattern, handlers)
+
+    route_pattern = url_path_join(base_url, application_url, "clusterList")
+    handlers = [(route_pattern, ClusterListController)]
+    web_app.add_handlers(host_pattern, handlers)
+
+    route_pattern = url_path_join(base_url, application_url, "runtimeList")
+    handlers = [(route_pattern, RuntimeController)]
+    web_app.add_handlers(host_pattern, handlers)
+
+    route_pattern = url_path_join(base_url, application_url, "createJobScheduler")
+    handlers = [(route_pattern, ExecutorController)]
+    web_app.add_handlers(host_pattern, handlers)
+
+    route_pattern_dag = url_path_join(base_url, application_url, "dagList")
+    route_pattern_download = url_path_join(base_url, application_url, "dagDownload")
+    route_pattern_delete = url_path_join(base_url, application_url, "dagDelete")
+    route_pattern_patch = url_path_join(base_url, application_url, "dagUpdate")
+    handlers = [
+        (route_pattern_dag, DagListController),
+        (route_pattern_download, DagDownloadController),
+        (route_pattern_delete, DagDeleteController),
+        (route_pattern_patch, DagUpdateController),
+    ]
+    web_app.add_handlers(host_pattern, handlers)
+
+    route_pattern = url_path_join(base_url, application_url, "editJobScheduler")
+    handlers = [(route_pattern, EditDagController)]
     web_app.add_handlers(host_pattern, handlers)
diff --git a/dataproc_jupyter_plugin/models/models.py b/dataproc_jupyter_plugin/models/models.py
new file mode 100644
index 00000000..518e23f8
--- /dev/null
+++ b/dataproc_jupyter_plugin/models/models.py
@@ -0,0 +1,59 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+
+
+from typing import Dict, List, Optional
+from pydantic import BaseModel
+
+
+class ComposerEnvironment(BaseModel):
+    """Defines a runtime context where job
+    execution will happen. For example, conda
+    environment.
+    """
+
+    name: str
+    label: str
+    description: str
+    file_extensions: List[str]  # Supported input file types
+    metadata: Optional[Dict[str, str]]  # Optional metadata
+
+    def __str__(self):
+        return self.json()
+
+
+class DescribeJob(BaseModel):
+    input_filename: str = None
+    composer_environment_name: str = None
+    output_formats: Optional[List[str]] = None
+    parameters: Optional[List[str]] = None
+    selected_mode: str = None
+    serverless_name: object = None
+    cluster_name: str = None
+    mode_selected: str = None
+    schedule_value: str = None
+    retry_count: int = 2
+    retry_delay: int = 5
+    email_failure: bool = False
+    email_delay: bool = False
+    email: Optional[List[str]] = None
+    name: str = None
+    dag_id: str = None
+    stop_cluster: bool = False
+    time_zone: str = None
+    email_success: bool = False
+
+    @classmethod
+    def from_dict(cls, data):
+        return cls(**data)
diff --git a/dataproc_jupyter_plugin/services/clusterListService.py b/dataproc_jupyter_plugin/services/clusterListService.py
new file mode 100644
index 00000000..39ad2bc3
--- /dev/null
+++ b/dataproc_jupyter_plugin/services/clusterListService.py
@@ -0,0 +1,46 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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 requests
+from dataproc_jupyter_plugin.utils.constants import CONTENT_TYPE, dataproc_url
+
+
+class ClusterListService:
+    def list_clusters(self, credentials, page_size, page_token, log):
+        try:
+            if (
+                ("access_token" in credentials)
+                and ("project_id" in credentials)
+                and ("region_id" in credentials)
+            ):
+                access_token = credentials["access_token"]
+                project_id = credentials["project_id"]
+                region_id = credentials["region_id"]
+                api_endpoint = f"{dataproc_url}/v1/projects/{project_id}/regions/{region_id}/clusters?pageSize={page_size}&pageToken={page_token}"
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                }
+                response = requests.get(api_endpoint, headers=headers)
+                if response.status_code == 200:
+                    resp = response.json()
+
+                return resp
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+        except Exception as e:
+            log.exception(f"Error fetching cluster list")
+            return {"error": str(e)}
diff --git a/dataproc_jupyter_plugin/services/composerService.py b/dataproc_jupyter_plugin/services/composerService.py
new file mode 100644
index 00000000..be19eb40
--- /dev/null
+++ b/dataproc_jupyter_plugin/services/composerService.py
@@ -0,0 +1,75 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+
+
+from typing import Dict, List
+import requests
+from dataproc_jupyter_plugin.models.models import ComposerEnvironment
+from dataproc_jupyter_plugin.utils.constants import CONTENT_TYPE, ENVIRONMENT_API
+
+
+class ComposerService:
+    def list_environments(self, credentials, log) -> List[ComposerEnvironment]:
+        try:
+            if (
+                ("access_token" in credentials)
+                and ("project_id" in credentials)
+                and ("region_id" in credentials)
+            ):
+                access_token = credentials["access_token"]
+                project_id = credentials["project_id"]
+                region_id = credentials["region_id"]
+                environments = []
+                api_endpoint = f"{ENVIRONMENT_API}/projects/{project_id}/locations/{region_id}/environments"
+
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                }
+                response = requests.get(api_endpoint, headers=headers)
+                if response.status_code == 200:
+                    resp = response.json()
+                    if not resp:
+                        return environments
+                    else:
+                        environment = resp["environments"]
+                        # Extract the 'name' values from the 'environments' list
+                        names = [env["name"] for env in environment]
+                        # Extract the last value after the last slash for each 'name'
+                        last_values = [name.split("/")[-1] for name in names]
+                        for env in last_values:
+                            name = env
+                            environments.append(
+                                ComposerEnvironment(
+                                    name=name,
+                                    label=name,
+                                    description=f"Environment: {name}",
+                                    file_extensions=["ipynb"],
+                                    metadata={"path": env},
+                                )
+                            )
+                        return environments
+                else:
+                    log.exception(f"Error listing environments")
+                    print(f"Error: {response.status_code} - {response.text}")
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+
+        except FileNotFoundError:
+            environments = []
+            return environments
+
+    def manage_environments_command(self) -> str:
+        return ""
diff --git a/dataproc_jupyter_plugin/services/dagListService.py b/dataproc_jupyter_plugin/services/dagListService.py
new file mode 100644
index 00000000..917bc583
--- /dev/null
+++ b/dataproc_jupyter_plugin/services/dagListService.py
@@ -0,0 +1,137 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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 subprocess
+import requests
+from dataproc_jupyter_plugin.services.composerService import ENVIRONMENT_API
+from dataproc_jupyter_plugin.utils.constants import CONTENT_TYPE
+
+
+class DagListService:
+    def get_airflow_uri(self, composer_name, credentials, log):
+        try:
+            if (
+                ("access_token" in credentials)
+                and ("project_id" in credentials)
+                and ("region_id" in credentials)
+            ):
+                access_token = credentials["access_token"]
+                project_id = credentials["project_id"]
+                region_id = credentials["region_id"]
+                api_endpoint = f"{ENVIRONMENT_API}/projects/{project_id}/locations/{region_id}/environments/{composer_name}"
+
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                }
+                response = requests.get(api_endpoint, headers=headers)
+                if response.status_code == 200:
+                    resp = response.json()
+                    airflow_uri = resp.get("config", {}).get("airflowUri", "")
+                    bucket = resp.get("storageConfig", {}).get("bucket", "")
+                    return airflow_uri, bucket
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+        except Exception as e:
+            log.exception(f"Error getting airflow uri: {str(e)}")
+            print(f"Error: {e}")
+
+    def list_jobs(self, credentials, composer_name, tags, log):
+        airflow_uri, bucket = DagListService.get_airflow_uri(
+            self, composer_name, credentials, log
+        )
+        try:
+            if "access_token" in credentials:
+                access_token = credentials["access_token"]
+                api_endpoint = f"{airflow_uri}/api/v1/dags?tags={tags}"
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                }
+                response = requests.get(api_endpoint, headers=headers)
+                if response.status_code == 200:
+                    resp = response.json()
+                return resp, bucket
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+        except Exception as e:
+            log.exception(f"Error getting dag list: {str(e)}")
+            return {"error": str(e)}
+
+
+class DagDeleteService:
+    def delete_job(self, credentials, composer_name, dag_id, log):
+        airflow_uri, bucket = DagListService.get_airflow_uri(
+            self, composer_name, credentials, log
+        )
+        try:
+            if "access_token" in credentials:
+                access_token = credentials["access_token"]
+                api_endpoint = f"{airflow_uri}/api/v1/dags/{dag_id}"
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                }
+                response = requests.delete(api_endpoint, headers=headers)
+                log.info(response)
+                cmd = f"gsutil rm gs://{bucket}/dags/dag_{dag_id}.py"
+                process = subprocess.Popen(
+                    cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
+                )
+                output, _ = process.communicate()
+                if process.returncode == 0:
+                    return 0
+                else:
+                    log.exception(f"Error deleting dag")
+                    return 1
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+        except Exception as e:
+            log.exception(f"Error deleting dag: {str(e)}")
+            return {"error": str(e)}
+
+
+class DagUpdateService:
+    def update_job(self, credentials, composer_name, dag_id, status, log):
+        airflow_uri, bucket = DagListService.get_airflow_uri(
+            self, composer_name, credentials, log
+        )
+        try:
+            if "access_token" in credentials:
+                access_token = credentials["access_token"]
+                api_endpoint = f"{airflow_uri}/api/v1/dags/{dag_id}"
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                }
+                if status == "true":
+                    data = {"is_paused": False}
+                else:
+                    data = {"is_paused": True}
+                response = requests.patch(api_endpoint, json=data, headers=headers)
+                if response.status_code == 200:
+                    return 0
+                else:
+                    log.exception(f"Error updating status")
+                    return 1
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+        except Exception as e:
+            log.exception(f"Error updating status: {str(e)}")
+            return {"error": str(e)}
diff --git a/dataproc_jupyter_plugin/services/dagRunService.py b/dataproc_jupyter_plugin/services/dagRunService.py
new file mode 100644
index 00000000..c747efb2
--- /dev/null
+++ b/dataproc_jupyter_plugin/services/dagRunService.py
@@ -0,0 +1,121 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+
+
+from dataproc_jupyter_plugin.services.dagListService import DagListService
+import requests
+from dataproc_jupyter_plugin.services.composerService import ENVIRONMENT_API
+from dataproc_jupyter_plugin.utils.constants import CONTENT_TYPE
+
+
+class DagRunListService:
+    def list_dag_runs(
+        self, credentials, composer_name, dag_id, start_date, end_date, log
+    ):
+        airflow_uri, bucket = DagListService.get_airflow_uri(
+            self, composer_name, credentials, log
+        )
+        try:
+            if (
+                ("access_token" in credentials)
+                and ("project_id" in credentials)
+                and ("region_id" in credentials)
+            ):
+                access_token = credentials["access_token"]
+                api_endpoint = f"{airflow_uri}/api/v1/dags/{dag_id}/dagRuns?execution_date_gte={start_date}&execution_date_lte={end_date}"
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                }
+                response = requests.get(api_endpoint, headers=headers)
+                if response.status_code == 200:
+                    resp = response.json()
+
+                return resp
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+        except Exception as e:
+            log.exception(f"Error fetching dag run list: {str(e)}")
+            return {"error": str(e)}
+
+
+class DagRunTaskListService:
+    def list_dag_run_task(self, credentials, composer_name, dag_id, dag_run_id, log):
+        airflow_uri, bucket = DagListService.get_airflow_uri(
+            self, composer_name, credentials, log
+        )
+        try:
+            if (
+                ("access_token" in credentials)
+                and ("project_id" in credentials)
+                and ("region_id" in credentials)
+            ):
+                access_token = credentials["access_token"]
+                api_endpoint = f"{airflow_uri}/api/v1/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances"
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                }
+                response = requests.get(api_endpoint, headers=headers)
+                if response.status_code == 200:
+                    resp = response.json()
+
+                return resp
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+        except Exception as e:
+            log.exception(f"Error fetching dag run task list: {str(e)}")
+            return {"error": str(e)}
+
+
+class DagRunTaskLogsListService:
+    def list_dag_run_task_logs(
+        self,
+        credentials,
+        composer_name,
+        dag_id,
+        dag_run_id,
+        task_id,
+        task_try_number,
+        log,
+    ):
+        airflow_uri, bucket = DagListService.get_airflow_uri(
+            self, composer_name, credentials, log
+        )
+        try:
+            if (
+                ("access_token" in credentials)
+                and ("project_id" in credentials)
+                and ("region_id" in credentials)
+            ):
+                access_token = credentials["access_token"]
+                api_endpoint = f"{airflow_uri}/api/v1/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/logs/{task_try_number}"
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                }
+                response = requests.get(api_endpoint, headers=headers)
+                if response.status_code == 200:
+                    resp = response.text
+                    resp_to_json = {"content": resp}
+
+                return resp_to_json
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+        except Exception as e:
+            log.exception(f"Error fetching dag run task logs: {str(e)}")
+            return {"error": str(e)}
diff --git a/dataproc_jupyter_plugin/services/editDagService.py b/dataproc_jupyter_plugin/services/editDagService.py
new file mode 100644
index 00000000..c3160752
--- /dev/null
+++ b/dataproc_jupyter_plugin/services/editDagService.py
@@ -0,0 +1,166 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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 subprocess
+import os
+import re
+import requests
+import urllib
+from dataproc_jupyter_plugin.utils.constants import CONTENT_TYPE, storage_url
+
+
+class DagEditService:
+    def get_dag_file(self, credentials, dag_id, bucket_name, log):
+        try:
+            if ("access_token" in credentials) and ("project_id" in credentials):
+                access_token = credentials["access_token"]
+                project_id = credentials["project_id"]
+                file_path = f"dags/dag_{dag_id}.py"
+                encoded_path = urllib.parse.quote(file_path, safe="")
+                api_endpoint = (
+                    f"{storage_url}b/{bucket_name}/o/{encoded_path}?alt=media"
+                )
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                    "X-Goog-User-Project": project_id,
+                }
+                response = requests.get(api_endpoint, headers=headers)
+                if response.status_code == 200:
+                    log.info("Dag file response fetched")
+
+                return response.content
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+        except Exception as e:
+            log.exception(f"Error reading dag file: {str(e)}")
+            return {"error": str(e)}
+
+    def edit_jobs(self, dag_id, bucket_name, credentials, log):
+        try:
+            cluster_name = ""
+            serverless_name = ""
+            email_on_success = "False"
+            stop_cluster_check = "False"
+            mode_selected = "serverless"
+            time_zone = ""
+            pattern = r"parameters\s*=\s*'''(.*?)'''"
+            file_response = DagEditService.get_dag_file(
+                self, credentials, dag_id, bucket_name, log
+            )
+            content_str = file_response.decode("utf-8")
+            file_content = re.sub(r"(?<!\\)\\(?!n)", "", content_str)
+
+            if file_content:
+                for line in file_content.split("\n"):
+                    if "input_notebook" in line:
+                        input_notebook = line.split("=")[-1].strip().strip("'\"")
+                        break
+
+                for line in file_content.split("\n"):
+                    match = re.search(pattern, file_content, re.DOTALL)
+                    if match:
+                        parameters_yaml = match.group(1)
+                        parameters_list = [
+                            line.strip()
+                            for line in parameters_yaml.split("\n")
+                            if line.strip()
+                        ]
+                    else:
+                        parameters_list = []
+
+                for line in file_content.split("\n"):
+                    if "email" in line:
+                        # Extract the email string from the line
+                        email_str = (
+                            line.split(":")[-1].strip().strip("'\"").replace(",", "")
+                        )
+                        # Use regular expression to extract email addresses
+                        email_list = re.findall(
+                            r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b",
+                            email_str,
+                        )
+                        # Remove quotes from email addresses
+                        email_list = [email.strip("'\"") for email in email_list]
+                        break
+                for line in file_content.split("\n"):
+                    if "cluster_name" in line:
+                        cluster_name = (
+                            line.split(":")[-1]
+                            .strip()
+                            .strip("'\"}")
+                            .split("'")[0]
+                            .strip()
+                        )  # Extract project_id from the line
+                    elif "submit_pyspark_job" in line:
+                        mode_selected = "cluster"
+                    elif "'retries'" in line:
+                        retries = line.split(":")[-1].strip().strip("'\"},")
+                        retry_count = int(
+                            retries.strip("'\"")
+                        )  # Extract retry_count from the line
+                    elif "retry_delay" in line:
+                        retry_delay = int(
+                            line.split("int('")[1].split("')")[0]
+                        )  # Extract retry_delay from the line
+                    elif "email_on_failure" in line:
+                        second_part = line.split(":")[1].strip()
+                        email_on_failure = second_part.split("'")[
+                            1
+                        ]  # Extract email_failure from the line
+                    elif "email_on_retry" in line:
+                        second_part = line.split(":")[1].strip()
+                        email_on_retry = second_part.split("'")[1]
+                    elif "email_on_success" in line:
+                        second_part = line.split(":")[1].strip()
+                        email_on_success = second_part.split("'")[1]
+                    elif "schedule_interval" in line:
+                        schedule_interval = (
+                            line.split("=")[-1]
+                            .strip()
+                            .strip("'\"")
+                            .split(",")[0]
+                            .rstrip("'\"")
+                        )  # Extract schedule_interval from the line
+                    elif "stop_cluster_check" in line:
+                        stop_cluster_check = line.split("=")[-1].strip().strip("'\"")
+                    elif "serverless_name" in line:
+                        serverless_name = line.split("=")[-1].strip().strip("'\"")
+                    elif "time_zone" in line:
+                        time_zone = line.split("=")[-1].strip().strip("'\"")
+
+                payload = {
+                    "input_filename": input_notebook,
+                    "parameters": parameters_list,
+                    "mode_selected": mode_selected,
+                    "cluster_name": cluster_name,
+                    "serverless_name": serverless_name,
+                    "retry_count": retry_count,
+                    "retry_delay": retry_delay,
+                    "email_failure": email_on_failure,
+                    "email_delay": email_on_retry,
+                    "email_success": email_on_success,
+                    "email": email_list,
+                    "schedule_value": schedule_interval,
+                    "stop_cluster": stop_cluster_check,
+                    "time_zone": time_zone,
+                }
+                return payload
+
+            else:
+                log.exception(f"No Dag file found")
+        except Exception as e:
+            log.exception(f"Error downloading dag file: {str(e)}")
diff --git a/dataproc_jupyter_plugin/services/executorService.py b/dataproc_jupyter_plugin/services/executorService.py
new file mode 100644
index 00000000..fdcc9610
--- /dev/null
+++ b/dataproc_jupyter_plugin/services/executorService.py
@@ -0,0 +1,260 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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
+import requests
+from dataproc_jupyter_plugin.services.composerService import ENVIRONMENT_API
+from dataproc_jupyter_plugin.models.models import DescribeJob
+import nbformat
+import subprocess
+from jinja2 import Environment, PackageLoader, select_autoescape
+import uuid
+from datetime import datetime, timedelta
+from dataproc_jupyter_plugin.utils.constants import CONTENT_TYPE, GCS
+import pendulum
+
+unique_id = str(uuid.uuid4().hex)
+job_id = ""
+job_name = ""
+TEMPLATES_FOLDER_PATH = "dagTemplates"
+ROOT_FOLDER = "dataproc_jupyter_plugin"
+
+
+def get_bucket(runtime_env, credentials, log):
+    try:
+        if (
+            ("access_token" in credentials)
+            and ("project_id" in credentials)
+            and ("region_id" in credentials)
+        ):
+            access_token = credentials["access_token"]
+            project_id = credentials["project_id"]
+            region_id = credentials["region_id"]
+            api_endpoint = f"{ENVIRONMENT_API}/projects/{project_id}/locations/{region_id}/environments/{runtime_env}"
+
+            headers = {
+                "Content-Type": CONTENT_TYPE,
+                "Authorization": f"Bearer {access_token}",
+            }
+
+            response = requests.get(api_endpoint, headers=headers)
+            if response.status_code == 200:
+                resp = response.json()
+                gcs_dag_path = resp.get("storageConfig", {}).get("bucket", "")
+                return gcs_dag_path
+        else:
+            log.exception(f"Missing required credentials")
+            raise ValueError("Missing required credentials")
+    except Exception as e:
+        log.exception(f"Error getting bucket name: {str(e)}")
+        print(f"Error: {e}")
+
+
+def check_file_exists(bucket, file_path, log):
+    cmd = f"gsutil ls gs://{bucket}/dataproc-notebooks/{file_path}"
+    process = subprocess.Popen(
+        cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
+    )
+    output, error = process.communicate()
+    if process.returncode == 0:
+        return True
+    else:
+        if "matched no objects" in error.decode():
+            return False
+        else:
+            log.exception(f"Error cheking file existence: {error.decode()}")
+            raise FileNotFoundError(error.decode)
+
+
+class ExecutorService:
+    """Default execution manager that executes notebooks"""
+
+    @staticmethod
+    def upload_dag_to_gcs(dag_file, credentials, gcs_dag_bucket, log):
+        if "region_id" in credentials:
+            cmd = f"gsutil cp '{dag_file}' gs://{gcs_dag_bucket}/dags/"
+            process = subprocess.Popen(
+                cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
+            )
+            output, error = process.communicate()
+        if process.returncode == 0:
+            log.info(f"Dag file uploaded to gcs successfully")
+            os.remove(dag_file)
+        if process.returncode != 0:
+            log.exception(f"Error uploading dag file to gcs: {error.decode()}")
+            raise IOError(error.decode)
+
+    @staticmethod
+    def upload_input_file_to_gcs(input, gcs_dag_bucket, job_name, log):
+        cmd = f"gsutil cp './{input}' gs://{gcs_dag_bucket}/dataproc-notebooks/{job_name}/input_notebooks/"
+        process = subprocess.Popen(
+            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
+        )
+        output, error = process.communicate()
+        if process.returncode == 0:
+            log.info(f"Input file uploaded to gcs successfully")
+        else:
+            log.exception(f"Error uploading input file to gcs: {error.decode()}")
+            raise IOError(error.decode)
+
+    @staticmethod
+    def upload_papermill_to_gcs(gcs_dag_bucket, log):
+        env = Environment(
+            loader=PackageLoader("dataproc_jupyter_plugin", "dagTemplates"),
+            autoescape=select_autoescape(["py"]),
+        )
+        wrapper_papermill_path = env.get_template("wrapper_papermill.py").filename
+        cmd = f"gsutil cp '{wrapper_papermill_path}' gs://{gcs_dag_bucket}/dataproc-notebooks/"
+        process = subprocess.Popen(
+            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
+        )
+        output, error = process.communicate()
+        print(process.returncode, error, output)
+        if process.returncode == 0:
+            log.info(f"Papermill file uploaded to gcs successfully")
+            print(process.returncode, error, output)
+        else:
+            log.exception(f"Error uploading papermill file to gcs: {error.decode()}")
+            raise IOError(error.decode)
+
+    @staticmethod
+    def prepare_dag(job, gcs_dag_bucket, dag_file, credentials, log):
+        log.info(f"Generating dag file")
+        DAG_TEMPLATE_CLUSTER_V1 = "pysparkJobTemplate-v1.txt"
+        DAG_TEMPLATE_SERVERLESS_V1 = "pysparkBatchTemplate-v1.txt"
+        environment = Environment(
+            loader=PackageLoader("dataproc_jupyter_plugin", TEMPLATES_FOLDER_PATH)
+        )
+        if ("project_id" in credentials) and ("region_id" in credentials):
+            gcp_project_id = credentials["project_id"]
+            gcp_region_id = credentials["region_id"]
+        cmd = "gcloud config get-value account"
+        process = subprocess.Popen(
+            cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
+        )
+        user, error = process.communicate()
+        owner = user.split("@")[0]  # getting username from email
+        if job.schedule_value == "":
+            schedule_interval = "@once"
+        else:
+            schedule_interval = job.schedule_value
+        if job.time_zone == "":
+            yesterday = datetime.combine(
+                datetime.today() - timedelta(1), datetime.min.time()
+            )
+            start_date = yesterday
+            time_zone = ""
+        else:
+            yesterday = pendulum.now().subtract(days=1)
+            desired_timezone = job.time_zone
+            dag_timezone = pendulum.timezone(desired_timezone)
+            start_date = yesterday.replace(tzinfo=dag_timezone)
+            time_zone = job.time_zone
+        if len(job.parameters) != 0:
+            parameters = "\n".join(item.replace(":", ": ") for item in job.parameters)
+        else:
+            parameters = ""
+        if job.mode_selected == "cluster":
+            template = environment.get_template(DAG_TEMPLATE_CLUSTER_V1)
+            if not job.input_filename.startswith(GCS):
+                input_notebook = f"gs://{gcs_dag_bucket}/dataproc-notebooks/{job.name}/input_notebooks/{job.input_filename}"
+            else:
+                input_notebook = job.input_filename
+            content = template.render(
+                job,
+                inputFilePath=f"gs://{gcs_dag_bucket}/dataproc-notebooks/wrapper_papermill.py",
+                gcpProjectId=gcp_project_id,
+                gcpRegion=gcp_region_id,
+                input_notebook=input_notebook,
+                output_notebook=f"gs://{gcs_dag_bucket}/dataproc-output/{job.name}/output-notebooks/{job.name}_{job.dag_id}.ipynb",
+                owner=owner,
+                schedule_interval=schedule_interval,
+                start_date=start_date,
+                parameters=parameters,
+                time_zone=time_zone,
+            )
+        else:
+            template = environment.get_template(DAG_TEMPLATE_SERVERLESS_V1)
+            job_dict = job.dict()
+            phs_path = (
+                job_dict.get("serverless_name", {})
+                .get("environmentConfig", {})
+                .get("peripheralsConfig", {})
+                .get("sparkHistoryServerConfig", {})
+                .get("dataprocCluster", "")
+            )
+            serverless_name = (
+                job_dict.get("serverless_name", {})
+                .get("jupyterSession", {})
+                .get("displayName", "")
+            )
+            custom_container = (
+                job_dict.get("serverless_name", {})
+                .get("runtimeConfig", {})
+                .get("containerImage", "")
+            )
+            metastore_service = (
+                job_dict.get("serverless_name", {})
+                .get("environmentConfig", {})
+                .get("peripheralsConfig", {})
+                .get("metastoreService", {})
+            )
+            if not job.input_filename.startswith(GCS):
+                input_notebook = f"gs://{gcs_dag_bucket}/dataproc-notebooks/{job.name}/input_notebooks/{job.input_filename}"
+            else:
+                input_notebook = job.input_filename
+            content = template.render(
+                job,
+                inputFilePath=f"gs://{gcs_dag_bucket}/dataproc-notebooks/wrapper_papermill.py",
+                gcpProjectId=gcp_project_id,
+                gcpRegion=gcp_region_id,
+                input_notebook=input_notebook,
+                output_notebook=f"gs://{gcs_dag_bucket}/dataproc-output/{job.name}/output-notebooks/{job.name}_{job.dag_id}.ipynb",
+                owner=owner,
+                schedule_interval=schedule_interval,
+                start_date=start_date,
+                parameters=parameters,
+                phs_path=phs_path,
+                serverless_name=serverless_name,
+                time_zone=time_zone,
+                custom_container=custom_container,
+                metastore_service=metastore_service,
+            )
+
+        print(content)
+        with open(dag_file, mode="w", encoding="utf-8") as message:
+            message.write(content)
+
+    def execute(self, credentials, input_data, log):
+        job = DescribeJob(**input_data)
+        global job_id
+        global job_name
+        job_id = job.dag_id
+        job_name = job.name
+        dag_file = f"dag_{job_name}.py"
+        gcs_dag_bucket = get_bucket(job.composer_environment_name, credentials, log)
+        remote_file_path = "wrapper_papermill.py"
+
+        if check_file_exists(gcs_dag_bucket, remote_file_path, log):
+            print(f"The file gs://{gcs_dag_bucket}/{remote_file_path} exists.")
+        else:
+            self.upload_papermill_to_gcs(gcs_dag_bucket, log)
+            print(f"The file gs://{gcs_dag_bucket}/{remote_file_path} does not exist.")
+        if not job.input_filename.startswith(GCS):
+            self.upload_input_file_to_gcs(
+                job.input_filename, gcs_dag_bucket, job_name, log
+            )
+        self.prepare_dag(job, gcs_dag_bucket, dag_file, credentials, log)
+        self.upload_dag_to_gcs(dag_file, credentials, gcs_dag_bucket, log)
diff --git a/dataproc_jupyter_plugin/services/gcpUrlService.py b/dataproc_jupyter_plugin/services/gcpUrlService.py
new file mode 100644
index 00000000..4bd82cd6
--- /dev/null
+++ b/dataproc_jupyter_plugin/services/gcpUrlService.py
@@ -0,0 +1,25 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+
+
+from google.cloud.jupyter_config.config import get_gcloud_config
+
+
+def gcp_service_url(service_name, default_url=None):
+    default_url = default_url or f"https://{service_name}.googleapis.com/"
+    configured_url = get_gcloud_config(
+        f"configuration.properties.api_endpoint_overrides.{service_name}"
+    )
+    url = configured_url or default_url
+    return url
diff --git a/dataproc_jupyter_plugin/services/runtimeListService.py b/dataproc_jupyter_plugin/services/runtimeListService.py
new file mode 100644
index 00000000..b30c8e3f
--- /dev/null
+++ b/dataproc_jupyter_plugin/services/runtimeListService.py
@@ -0,0 +1,47 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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 requests
+from dataproc_jupyter_plugin.utils.constants import CONTENT_TYPE, dataproc_url
+
+
+class RuntimeListService:
+    def list_runtime(self, credentials, page_size, page_token, log):
+        try:
+            if (
+                ("access_token" in credentials)
+                and ("project_id" in credentials)
+                and ("region_id" in credentials)
+            ):
+                access_token = credentials["access_token"]
+                project_id = credentials["project_id"]
+                region_id = credentials["region_id"]
+                api_endpoint = f"{dataproc_url}/v1/projects/{project_id}/locations/{region_id}/sessionTemplates?pageSize={page_size}&pageToken={page_token}"
+
+                headers = {
+                    "Content-Type": CONTENT_TYPE,
+                    "Authorization": f"Bearer {access_token}",
+                }
+                response = requests.get(api_endpoint, headers=headers)
+                if response.status_code == 200:
+                    resp = response.json()
+
+                return resp
+            else:
+                log.exception(f"Missing required credentials")
+                raise ValueError("Missing required credentials")
+        except Exception as e:
+            log.exception(f"Error fetching runtime list: {str(e)}")
+            return {"error": str(e)}
diff --git a/dataproc_jupyter_plugin/utils/constants.py b/dataproc_jupyter_plugin/utils/constants.py
new file mode 100644
index 00000000..1a8fcc55
--- /dev/null
+++ b/dataproc_jupyter_plugin/utils/constants.py
@@ -0,0 +1,35 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+
+
+from dataproc_jupyter_plugin.services.gcpUrlService import gcp_service_url
+
+ENVIRONMENT_API = "https://composer.googleapis.com/v1"
+TAGS = "dataproc_jupyter_plugin"
+GCS = "gs://"
+CONTENT_TYPE = "application/json"
+
+
+dataproc_url = gcp_service_url("dataproc")
+dataproc_url = gcp_service_url("dataproc")
+compute_url = gcp_service_url(
+    "compute", default_url="https://compute.googleapis.com/compute/v1"
+)
+metastore_url = gcp_service_url("metastore")
+cloudkms_url = gcp_service_url("cloudkms")
+cloudresourcemanager_url = gcp_service_url("cloudresourcemanager")
+datacatalog_url = gcp_service_url("datacatalog")
+storage_url = gcp_service_url(
+    "storage", default_url="https://storage.googleapis.com/storage/v1/"
+)
diff --git a/package.json b/package.json
index 476d8f7d..34ac30d1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "dataproc_jupyter_plugin",
-    "version": "0.1.72",
+    "version": "0.1.73",
     "description": "It is a plugin to work with dataproc services in Jupyterlab",
     "keywords": [
         "jupyter",
@@ -72,12 +72,17 @@
         "@jupyterlab/settingregistry": "^4.0.0",
         "@mui/icons-material": "^5.14.7",
         "@mui/material": "^5.14.7",
+        "@mui/x-date-pickers": "^6.18.6",
         "@types/uuid": "^9.0.2",
+        "antd": "^5.12.8",
+        "dayjs": "^1.11.10",
         "mui-chips-input": "^2.1.3",
         "react-arborist": "^3.1.0",
+        "react-js-cron": "^5.0.1",
         "react-spinners": "^0.13.8",
         "react-table": "^7.8.0",
         "react-toastify": "^9.1.3",
+        "tzdata": "^1.0.39",
         "yup": "^1.2.0"
     },
     "devDependencies": {
diff --git a/pyproject.toml b/pyproject.toml
index 5187158f..1ac07b15 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,6 +25,8 @@ dependencies = [
     "jupyter_server>=2.7.3,<3",
     "cachetools>=4.2.4",
     "kernels-mixer>=0.0.4",
+    "pendulum>=3.0.0",
+    "pydantic~=1.10.0"
 ]
 dynamic = ["version", "description", "authors", "urls", "keywords"]
 
diff --git a/src/batches/createBatch.tsx b/src/batches/createBatch.tsx
index 070571b8..6bef59c2 100644
--- a/src/batches/createBatch.tsx
+++ b/src/batches/createBatch.tsx
@@ -175,8 +175,6 @@ function CreateBatch({
         const parts = historyServerValue.split('/'); //splitting to take cluster name from project/projectName/region/regionName/cluster/clusterName
         historyServer = parts[parts.length - 1];
       }
-      batchInfoResponse?.environmentConfig?.peripheralsConfig
-        ?.sparkHistoryServerConfig?.dataprocCluster;
       metastoreService =
         batchInfoResponse?.environmentConfig?.peripheralsConfig
           ?.metastoreService || 'None';
@@ -813,8 +811,7 @@ function CreateBatch({
         ...(servicesSelected !== 'None' && {
           metastoreService: servicesSelected
         }),
-        ...(clusterSelected !== '' &&
-          clusterSelected !== '' && {
+        ...(clusterSelected !== '' && {
             sparkHistoryServerConfig: {
               dataprocCluster: `projects/${projectName}/regions/${regionName}/clusters/${clusterSelected}`
             } as SparkHistoryServerConfig
diff --git a/src/controls/NotebookButtonExtension.tsx b/src/controls/NotebookButtonExtension.tsx
index 734c48d9..801a98e2 100644
--- a/src/controls/NotebookButtonExtension.tsx
+++ b/src/controls/NotebookButtonExtension.tsx
@@ -29,6 +29,8 @@ import { authenticatedFetch } from '../utils/utils';
 import { HTTP_METHOD, SPARK_HISTORY_SERVER } from '../utils/const';
 import { SessionTemplate } from '../sessions/sessionTemplate';
 import serverlessIcon from '../../style/icons/serverless_icon.svg';
+import notebookSchedulerIcon from '../../style/icons/notebook_scheduler.svg';
+import { NotebookScheduler } from '../scheduler/notebookScheduler';
 
 const iconLogs = new LabIcon({
   name: 'launcher:logs-icon',
@@ -42,6 +44,10 @@ const iconServerless = new LabIcon({
   name: 'launcher:serverless-icon',
   svgstr: serverlessIcon
 });
+const iconNotebookScheduler = new LabIcon({
+  name: 'launcher:notebook-scheduler-icon',
+  svgstr: notebookSchedulerIcon
+});
 
 /**
  * A disposable class to track the toolbar widget for a single notebook.
@@ -51,6 +57,7 @@ class NotebookButtonExtensionPoint implements IDisposable {
   isDisposed: boolean;
   private readonly sparkLogsButton: ToolbarButton;
   private readonly sessionDetailsButton: ToolbarButton;
+  private readonly notebookSchedulerButton: ToolbarButton;
   private sessionId?: string;
 
   /**
@@ -68,12 +75,11 @@ class NotebookButtonExtensionPoint implements IDisposable {
     this.isDisposed = false;
     this.context.sessionContext.sessionChanged.connect(this.onSessionChanged);
 
-
     this.sparkLogsButton = new ToolbarButton({
       icon: iconLogs,
       onClick: this.onSparkLogsClick,
       tooltip: 'Spark Logs',
-      className: "dark-theme-logs"
+      className: 'dark-theme-logs'
     });
     // TODO: we want to use the registry to specify a rank:
     // https://jupyterlab.readthedocs.io/en/stable/extension/extension_points.html#document-widgets
@@ -83,15 +89,38 @@ class NotebookButtonExtensionPoint implements IDisposable {
       icon: iconSessionLogs,
       onClick: this.onSessionDetailsClick,
       tooltip: 'Session Details',
-      className: "dark-theme-logs" 
+      className: 'dark-theme-logs'
     });
     this.panel.toolbar.insertItem(
       1000,
       'session-details',
       this.sessionDetailsButton
     );
+    this.notebookSchedulerButton = new ToolbarButton({
+      icon: iconNotebookScheduler,
+      onClick: () => this.onNotebookSchedulerClick(),
+      tooltip: 'Notebook Scheduler',
+      className: 'dark-theme-logs'
+    });
+    this.panel.toolbar.insertItem(
+      1000,
+      'notebook-scheduler',
+      this.notebookSchedulerButton
+    );
   }
 
+  private onNotebookSchedulerClick = () => {
+    const content = new NotebookScheduler(
+      this.app as JupyterLab,
+      this.themeManager,
+      this.context
+    );
+    const widget = new MainAreaWidget<NotebookScheduler>({ content });
+    widget.title.label = 'Notebook Scheduler';
+    widget.title.icon = iconNotebookScheduler;
+    this.app.shell.add(widget, 'main');
+  };
+
   private onSessionDetailsClick = () => {
     if (!this.sessionId) {
       //TODO: log error.
diff --git a/src/dpms/dpmsWidget.tsx b/src/dpms/dpmsWidget.tsx
index 715c48df..4554c58c 100644
--- a/src/dpms/dpmsWidget.tsx
+++ b/src/dpms/dpmsWidget.tsx
@@ -669,7 +669,7 @@ fetching database name from fully qualified name structure */
             'Error listing session details',
             LOG_LEVEL.ERROR
           );
-          toast.error('Failed to fetch session details'), toastifyCustomStyle;
+          toast.error('Failed to fetch session details', toastifyCustomStyle);
         });
     }
   };
diff --git a/src/gcs/gcsBrowserWidget.tsx b/src/gcs/gcsBrowserWidget.tsx
index f799a883..931df118 100644
--- a/src/gcs/gcsBrowserWidget.tsx
+++ b/src/gcs/gcsBrowserWidget.tsx
@@ -189,7 +189,7 @@ export class GcsBrowserWidget extends Widget {
 
     let newFolder = new ToolbarButton({
       icon: iconGCSNewFolder,
-      className: 'icon-white jp-NewFolderIcon',
+      className: 'icon-white',
       onClick: this.handleFolderCreation,
       tooltip: 'New Folder'
     });
diff --git a/src/index.ts b/src/index.ts
index 10d44a09..a1eff4df 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -27,11 +27,12 @@ import { LabIcon } from '@jupyterlab/ui-components';
 import { IMainMenu } from '@jupyterlab/mainmenu';
 import { Cluster } from './cluster/cluster';
 import { Batches } from './batches/batches';
-import { NotebookTemplates } from './notebookTemplates/notebookTemplates'
+import { NotebookTemplates } from './notebookTemplates/notebookTemplates';
 import clusterIcon from '../style/icons/cluster_icon.svg';
 import addRuntimeIcon from '../style/icons/add_runtime_template.svg';
 import serverlessIcon from '../style/icons/serverless_icon.svg';
 import notebookTemplateIcon from '../style/icons/notebook_template_icon.svg';
+import scheduledNotebooksIcon from '../style/icons/scheduled_notebooks_icon.svg';
 import storageIcon from '../style/icons/storage_icon.svg';
 import { Panel, Title, Widget } from '@lumino/widgets';
 import { AuthLogin } from './login/authLogin';
@@ -42,7 +43,10 @@ import dpmsIcon from '../style/icons/dpms_icon.svg';
 import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
 import { TITLE_LAUNCHER_CATEGORY } from './utils/const';
 import { RuntimeTemplate } from './runtime/runtimeTemplate';
-import { IFileBrowserFactory, IDefaultFileBrowser } from '@jupyterlab/filebrowser';
+import {
+  IFileBrowserFactory,
+  IDefaultFileBrowser
+} from '@jupyterlab/filebrowser';
 import dpmsIconDark from '../style/icons/dpms_icon_dark.svg';
 import storageIconDark from '../style/icons/Storage-icon-dark.svg';
 import { NotebookButtonExtension } from './controls/NotebookButtonExtension';
@@ -51,6 +55,7 @@ import { IDocumentManager } from '@jupyterlab/docmanager';
 import { GCSDrive } from './gcs/gcsDrive';
 import { GcsBrowserWidget } from './gcs/gcsBrowserWidget';
 import { DataprocLoggingService } from './utils/loggingService';
+import { NotebookScheduler } from './scheduler/notebookScheduler';
 
 const iconDpms = new LabIcon({
   name: 'launcher:dpms-icon',
@@ -76,7 +81,7 @@ const extension: JupyterFrontEndPlugin<void> = {
   activate: async (
     app: JupyterFrontEnd,
     factory: IFileBrowserFactory,
-    defaultFileBrowser:IDefaultFileBrowser,
+    defaultFileBrowser: IDefaultFileBrowser,
     launcher: ILauncher,
     mainMenu: IMainMenu,
     labShell: ILabShell,
@@ -100,6 +105,10 @@ const extension: JupyterFrontEndPlugin<void> = {
       name: 'launcher:serverless-icon',
       svgstr: serverlessIcon
     });
+    const iconScheduledNotebooks = new LabIcon({
+      name: 'launcher:scheduled-notebooks-icon',
+      svgstr: scheduledNotebooksIcon
+    });
     const iconNotebookTemplate = new LabIcon({
       name: 'launcher:notebook-template-icon',
       svgstr: notebookTemplateIcon
@@ -148,7 +157,7 @@ const extension: JupyterFrontEndPlugin<void> = {
       }
     };
     themeManager.themeChanged.connect(onThemeChanged);
-    
+
     /**
      * Helper method for when the preview flag gets updated.  This reads the
      * previewEnabled flag and hides or shows the GCS browser or DPMS explorer
@@ -283,7 +292,10 @@ const extension: JupyterFrontEndPlugin<void> = {
             newValue.title.label === 'Config Setup' ||
             newValue.title.label === 'Clusters' ||
             newValue.title.label === 'Serverless' ||
-            newValue.title.label === 'Settings') &&
+            newValue.title.label === 'Settings' ||
+            newValue.title.label === 'Notebook Templates' ||
+            newValue.title.label === 'Scheduled Jobs' ||
+            newValue.title.label === 'Notebook Scheduler') &&
           lastClusterName !== ''
         ) {
           localStorage.setItem('oldNotebookValue', lastClusterName || '');
@@ -298,7 +310,10 @@ const extension: JupyterFrontEndPlugin<void> = {
             newValue.title.label !== 'Clusters' &&
             newValue.title.label !== 'Serverless' &&
             newValue.title.label !== 'Runtime template' &&
-            newValue.title.label !== 'Settings'
+            newValue.title.label !== 'Settings' &&
+            newValue.title.label !== 'Notebook Templates' &&
+            newValue.title.label !== 'Scheduled Jobs' &&
+            newValue.title.label !== 'Notebook Scheduler'
           ) {
             let oldNotebook = localStorage.getItem('oldNotebookValue');
             localStorage.setItem('notebookValue', oldNotebook || '');
@@ -361,6 +376,23 @@ const extension: JupyterFrontEndPlugin<void> = {
         app.shell.add(widget, 'main');
       }
     });
+    const createNotebookJobsComponentCommand = 'create-notebook-jobs-component';
+    commands.addCommand(createNotebookJobsComponentCommand, {
+      caption: 'Create a new Serverless Component',
+      label: 'Scheduled Jobs',
+      icon: iconScheduledNotebooks,
+      execute: () => {
+        const content = new NotebookScheduler(
+          app as JupyterLab,
+          themeManager,
+          ''
+        );
+        const widget = new MainAreaWidget<NotebookScheduler>({ content });
+        widget.title.label = 'Scheduled Jobs';
+        widget.title.icon = iconScheduledNotebooks;
+        app.shell.add(widget, 'main');
+      }
+    });
 
     const createTemplateComponentCommand = 'create-template-component';
     commands.addCommand(createTemplateComponentCommand, {
@@ -369,7 +401,11 @@ const extension: JupyterFrontEndPlugin<void> = {
       // @ts-ignore jupyter lab icon command issue
       icon: args => (args['isPalette'] ? null : iconNotebookTemplate),
       execute: () => {
-        const content = new NotebookTemplates(app as JupyterLab, themeManager, defaultFileBrowser as IDefaultFileBrowser);
+        const content = new NotebookTemplates(
+          app as JupyterLab,
+          themeManager,
+          defaultFileBrowser as IDefaultFileBrowser
+        );
         const widget = new MainAreaWidget<NotebookTemplates>({ content });
         widget.title.label = 'Notebook Templates';
         widget.title.icon = iconNotebookTemplate;
@@ -494,6 +530,11 @@ const extension: JupyterFrontEndPlugin<void> = {
         category: TITLE_LAUNCHER_CATEGORY,
         rank: 3
       });
+      launcher.add({
+        command: createNotebookJobsComponentCommand,
+        category: TITLE_LAUNCHER_CATEGORY,
+        rank: 4
+      });
     }
 
     // the plugin depends on having a toast container, and Jupyter labs lazy
diff --git a/src/jobs/jobs.tsx b/src/jobs/jobs.tsx
index 52f1f059..c230c13c 100644
--- a/src/jobs/jobs.tsx
+++ b/src/jobs/jobs.tsx
@@ -271,17 +271,10 @@ function JobComponent({
               : undefined
           }
         >
-          {data.status.state === ClusterStatus.STATUS_RUNNING ? (
-            <iconDelete.react
-              tag="div"
-              className="icon-white logo-alignment-style"
-            />
-          ) : (
-            <iconDelete.react
-              tag="div"
-              className="icon-white logo-alignment-style"
-            />
-          )}
+          <iconDelete.react
+            tag="div"
+            className="icon-white logo-alignment-style"
+          />
         </div>
       </div>
     );
diff --git a/src/jobs/labelProperties.tsx b/src/jobs/labelProperties.tsx
index 76f52282..8a6f9ebe 100644
--- a/src/jobs/labelProperties.tsx
+++ b/src/jobs/labelProperties.tsx
@@ -64,7 +64,7 @@ function LabelProperties({
   labelDetailUpdated used to store the temporay label details when onchange
   */
   useEffect(() => {
-    if (!labelEditMode) {
+    if (!labelEditMode && labelDetail.length > 0 && !labelDetail[0].includes(':')) {
       if (
         buttonText === 'ADD LABEL' &&
         !selectedJobClone &&
diff --git a/src/scheduler/createNotebookScheduler.tsx b/src/scheduler/createNotebookScheduler.tsx
new file mode 100644
index 00000000..8cb5ac8a
--- /dev/null
+++ b/src/scheduler/createNotebookScheduler.tsx
@@ -0,0 +1,765 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ *
+ * 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 React, { useEffect, useState } from 'react';
+import { Input } from '../controls/MuiWrappedInput';
+import {
+  Autocomplete,
+  Checkbox,
+  FormControl,
+  FormControlLabel,
+  FormGroup,
+  Radio,
+  RadioGroup,
+  TextField,
+  Typography
+} from '@mui/material';
+import { MuiChipsInput } from 'mui-chips-input';
+import { IThemeManager } from '@jupyterlab/apputils';
+import { JupyterLab } from '@jupyterlab/application';
+import LabelProperties from '../jobs/labelProperties';
+import { v4 as uuidv4 } from 'uuid';
+import { Cron } from 'react-js-cron';
+import 'react-js-cron/dist/styles.css';
+import { KernelSpecAPI } from '@jupyterlab/services';
+import tzdata from 'tzdata';
+import { SchedulerService } from './schedulerServices';
+import NotebookJobComponent from './notebookJobs';
+import LeftArrowIcon from '../../style/icons/left_arrow_icon.svg';
+import { LabIcon } from '@jupyterlab/ui-components';
+import errorIcon from '../../style/icons/error_icon.svg';
+import { Button } from '@mui/material';
+import { scheduleMode } from '../utils/const';
+import { scheduleValueExpression } from '../utils/const';
+
+interface IDagList {
+  jobid: string;
+  notebookname: string;
+  schedule: string;
+  scheduleInterval: string;
+}
+
+const iconLeftArrow = new LabIcon({
+  name: 'launcher:left-arrow-icon',
+  svgstr: LeftArrowIcon
+});
+
+const iconError = new LabIcon({
+  name: 'launcher:error-icon',
+  svgstr: errorIcon
+});
+
+const CreateNotebookScheduler = ({
+  themeManager,
+  app,
+  context
+}: {
+  themeManager: IThemeManager;
+  app: JupyterLab;
+  context: any;
+}): JSX.Element => {
+  const [jobNameSelected, setJobNameSelected] = useState('');
+  const [inputFileSelected, setInputFileSelected] = useState('');
+  const [composerList, setComposerList] = useState<string[]>([]);
+  const [composerSelected, setComposerSelected] = useState('');
+
+  const [parameterDetail, setParameterDetail] = useState(['']);
+  const [parameterDetailUpdated, setParameterDetailUpdated] = useState(['']);
+  const [keyValidation, setKeyValidation] = useState(-1);
+  const [valueValidation, setValueValidation] = useState(-1);
+  const [duplicateKeyError, setDuplicateKeyError] = useState(-1);
+
+  const [selectedMode, setSelectedMode] = useState('cluster');
+  const [clusterList, setClusterList] = useState<string[]>([]);
+  const [serverlessList, setServerlessList] = useState<string[]>([]);
+  const [serverlessDataList, setServerlessDataList] = useState<string[]>([]);
+  const [clusterSelected, setClusterSelected] = useState('');
+  const [serverlessSelected, setServerlessSelected] = useState('');
+  const [serverlessDataSelected, setServerlessDataSelected] = useState({});
+  const [stopCluster, setStopCluster] = useState(false);
+
+  const [retryCount, setRetryCount] = useState<number | undefined>(2);
+  const [retryDelay, setRetryDelay] = useState<number | undefined>(5);
+  const [emailOnFailure, setEmailOnFailure] = useState(false);
+  const [emailOnRetry, setEmailonRetry] = useState(false);
+  const [emailOnSuccess, setEmailOnSuccess] = useState(false);
+  const [emailList, setEmailList] = useState<string[]>([]);
+
+  const [scheduleMode, setScheduleMode] = useState<scheduleMode>('runNow');
+  const [scheduleValue, setScheduleValue] = useState(scheduleValueExpression);
+  const [timeZoneSelected, setTimeZoneSelected] = useState(
+    Intl.DateTimeFormat().resolvedOptions().timeZone
+  );
+
+  const timezones = Object.keys(tzdata.zones).sort();
+
+  const [createCompleted, setCreateCompleted] =
+    context !== '' ? useState(false) : useState(true);
+  const [creatingScheduler, setCreatingScheduler] = useState(false);
+  const [jobNameValidation, setJobNameValidation] = useState(true);
+  const [jobNameSpecialValidation, setJobNameSpecialValidation] =
+    useState(false);
+  const [jobNameUniqueValidation, setJobNameUniqueValidation] = useState(true);
+  const [dagList, setDagList] = useState<IDagList[]>([]);
+  const [editMode, setEditMode] = useState(false);
+  const [dagListCall, setDagListCall] = useState(false);
+
+  const listClustersAPI = async () => {
+    await SchedulerService.listClustersAPIService(setClusterList);
+  };
+
+  const listSessionTemplatesAPI = async () => {
+    await SchedulerService.listSessionTemplatesAPIService(
+      setServerlessDataList,
+      setServerlessList
+    );
+  };
+
+  const listComposersAPI = async () => {
+    await SchedulerService.listComposersAPIService(setComposerList);
+  };
+
+  const handleComposerSelected = (data: string | null) => {
+    if (data) {
+      const selectedComposer = data.toString();
+      setComposerSelected(selectedComposer);
+      if (selectedComposer) {
+        const unique = getDaglist(selectedComposer);
+        if (!unique) {
+          setJobNameUniqueValidation(true);
+        }
+      }
+    }
+  };
+  const getDaglist = async (composer: string) => {
+    setDagListCall(true);
+    try {
+      await SchedulerService.listDagInfoAPIServiceForCreateNotebook(
+        setDagList,
+        composer
+      );
+      setDagListCall(false);
+      return true;
+    } catch (error) {
+      setDagListCall(false);
+      console.error('Error checking job name uniqueness:', error);
+      return false;
+    }
+  };
+
+  const handleSelectedModeChange = (
+    event: React.ChangeEvent<HTMLInputElement>
+  ) => {
+    setSelectedMode((event.target as HTMLInputElement).value);
+  };
+
+  const handleSchedulerModeChange = (
+    event: React.ChangeEvent<HTMLInputElement>
+  ) => {
+    const newValue = (event.target as HTMLInputElement).value;
+    setScheduleMode(newValue as scheduleMode);
+    if (newValue === 'runSchedule' && scheduleValue === '') {
+      setScheduleValue(scheduleValueExpression);
+    }
+  };
+
+  const handleClusterSelected = (data: string | null) => {
+    if (data) {
+      const selectedCluster = data.toString();
+      setClusterSelected(selectedCluster);
+    }
+  };
+
+  const handleTimeZoneSelected = (data: string | null) => {
+    if (data) {
+      const selectedTimeZone = data.toString();
+      setTimeZoneSelected(selectedTimeZone);
+    }
+  };
+
+  const handleServerlessSelected = (data: string | null) => {
+    if (data) {
+      const selectedServerless = data.toString();
+      const selectedData: any = serverlessDataList.filter((serverless: any) => {
+        return serverless.serverlessName === selectedServerless;
+      });
+      setServerlessDataSelected(selectedData[0].serverlessData);
+      setServerlessSelected(selectedServerless);
+    }
+  };
+
+  const handleStopCluster = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setStopCluster(event.target.checked);
+  };
+
+  const handleRetryCount = (data: number) => {
+    if (data >= 0) {
+      setRetryCount(data);
+    }
+  };
+
+  const handleRetryDelay = (data: number) => {
+    if (data >= 0) {
+      setRetryDelay(data);
+    }
+  };
+
+  const handleFailureChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setEmailOnFailure(event.target.checked);
+  };
+
+  const handleRetryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setEmailonRetry(event.target.checked);
+  };
+
+  const handleSuccessChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    setEmailOnSuccess(event.target.checked);
+  };
+
+  const handleEmailList = (data: string[]) => {
+    setEmailList(data);
+  };
+
+  const handleCreateJobScheduler = async () => {
+    let outputFormats = [];
+    outputFormats.push('ipynb');
+
+    let randomDagId = uuidv4();
+
+    const payload = {
+      input_filename: inputFileSelected,
+      composer_environment_name: composerSelected,
+      output_formats: outputFormats,
+      parameters: parameterDetailUpdated,
+      mode_selected: selectedMode,
+      retry_count: retryCount,
+      retry_delay: retryDelay,
+      email_failure: emailOnFailure,
+      email_delay: emailOnRetry,
+      email_success: emailOnSuccess,
+      email: emailList,
+      name: jobNameSelected,
+      schedule_value: scheduleMode === 'runNow' ? '' : scheduleValue,
+      stop_cluster: stopCluster,
+      dag_id: randomDagId,
+      time_zone: scheduleMode !== 'runNow' ? timeZoneSelected : '',
+      [selectedMode === 'cluster' ? 'cluster_name' : 'serverless_name']:
+        selectedMode === 'cluster' ? clusterSelected : serverlessDataSelected
+    };
+
+    await SchedulerService.createJobSchedulerService(
+      payload,
+      app,
+      setCreateCompleted,
+      setCreatingScheduler,
+      editMode
+    );
+    setEditMode(false);
+  };
+
+  const handleJobNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    event.target.value.length > 0
+      ? setJobNameValidation(true)
+      : setJobNameValidation(false);
+
+    //Regex to check job name must contain only letters, numbers, hyphens, and underscores
+    const regexp = /^[a-zA-Z0-9-_]+$/;
+    event.target.value.search(regexp)
+      ? setJobNameSpecialValidation(true)
+      : setJobNameSpecialValidation(false);
+    setJobNameSelected(event.target.value);
+
+    setJobNameSelected(event.target.value);
+  };
+
+  const isSaveDisabled = () => {
+    return (
+      dagListCall ||
+      creatingScheduler ||
+      jobNameSelected === '' ||
+      (!jobNameValidation && !editMode) ||
+      (jobNameSpecialValidation && !editMode) ||
+      (!jobNameUniqueValidation && !editMode) ||
+      inputFileSelected === '' ||
+      composerSelected === '' ||
+      (selectedMode === 'cluster' && clusterSelected === '') ||
+      (selectedMode === 'serverless' && serverlessSelected === '') ||
+      ((emailOnFailure || emailOnRetry || emailOnSuccess) &&
+        emailList.length === 0)
+    );
+  };
+
+  const handleCancel = async () => {
+    if (!editMode) {
+      setCreateCompleted(false);
+      app.shell.activeWidget?.close();
+    } else {
+      setCreateCompleted(true);
+    }
+  };
+
+  const getKernelDetail = async () => {
+    const kernelSpecs: any = await KernelSpecAPI.getSpecs();
+    const kernels = kernelSpecs.kernelspecs;
+    if (
+      kernels &&
+      context.sessionContext.kernelPreference.name &&
+      clusterList.length > 0 &&
+      serverlessDataList.length > 0
+    ) {
+      if (
+        kernels[
+          context.sessionContext.kernelPreference.name
+        ].resources.endpointParentResource.includes('/sessions')
+      ) {
+        const selectedData: any = serverlessDataList.filter(
+          (serverless: any) => {
+            return context.sessionContext.kernelDisplayName.includes(
+              serverless.serverlessName
+            );
+          }
+        );
+        setServerlessDataSelected(selectedData[0].serverlessData);
+        setServerlessSelected(selectedData[0].serverlessName);
+      } else {
+        const selectedData: any = clusterList.filter((cluster: string) => {
+          return context.sessionContext.kernelDisplayName.includes(cluster);
+        });
+        setClusterSelected(selectedData[0]);
+      }
+    }
+  };
+
+  useEffect(() => {
+    listComposersAPI();
+    listClustersAPI();
+    listSessionTemplatesAPI();
+    if (context !== '') {
+      setInputFileSelected(context.path);
+    }
+    setJobNameSelected('');
+  }, []);
+
+  useEffect(() => {
+    if (composerSelected !== '' && dagList.length > 0) {
+      const isUnique = !dagList.some(
+        dag => dag.notebookname === jobNameSelected
+      );
+      setJobNameUniqueValidation(isUnique);
+    }
+  }, [dagList, jobNameSelected, composerSelected]);
+
+  useEffect(() => {
+    if (context !== '') {
+      getKernelDetail();
+    }
+  }, [serverlessDataList]);
+
+  return (
+    <>
+      {createCompleted ? (
+        <NotebookJobComponent
+          app={app}
+          themeManager={themeManager}
+          composerSelectedFromCreate={composerSelected}
+          setCreateCompleted={setCreateCompleted}
+          setJobNameSelected={setJobNameSelected}
+          setComposerSelected={setComposerSelected}
+          setScheduleMode={setScheduleMode}
+          setScheduleValue={setScheduleValue}
+          setInputFileSelected={setInputFileSelected}
+          setParameterDetail={setParameterDetail}
+          setParameterDetailUpdated={setParameterDetailUpdated}
+          setSelectedMode={setSelectedMode}
+          setClusterSelected={setClusterSelected}
+          setServerlessSelected={setServerlessSelected}
+          setServerlessDataSelected={setServerlessDataSelected}
+          serverlessDataList={serverlessDataList}
+          setServerlessDataList={setServerlessDataList}
+          setServerlessList={setServerlessList}
+          setRetryCount={setRetryCount}
+          setRetryDelay={setRetryDelay}
+          setEmailOnFailure={setEmailOnFailure}
+          setEmailonRetry={setEmailonRetry}
+          setEmailOnSuccess={setEmailOnSuccess}
+          setEmailList={setEmailList}
+          setStopCluster={setStopCluster}
+          setTimeZoneSelected={setTimeZoneSelected}
+          setEditMode={setEditMode}
+        />
+      ) : (
+        <div>
+          <div className="cluster-details-header">
+            <div
+              role="button"
+              className="back-arrow-icon"
+              onClick={handleCancel}
+            >
+              <iconLeftArrow.react
+                tag="div"
+                className="icon-white logo-alignment-style"
+              />
+            </div>
+            {editMode ? (
+              <div className="create-job-scheduler-title">
+                Update A Scheduled Job
+              </div>
+            ) : (
+              <div className="create-job-scheduler-title">
+                Create A Scheduled Job
+              </div>
+            )}
+          </div>
+          <div className="submit-job-container">
+            <div className="create-scheduler-form-element">
+              <Input
+                className="create-scheduler-style"
+                value={jobNameSelected}
+                onChange={e => handleJobNameChange(e)}
+                type="text"
+                placeholder=""
+                Label="Job name*"
+                disabled={editMode}
+              />
+            </div>
+            {!jobNameValidation && !editMode && (
+              <div className="error-key-parent">
+                <iconError.react tag="div" className="logo-alignment-style" />
+                <div className="error-key-missing">Name is required</div>
+              </div>
+            )}
+            {jobNameSpecialValidation && jobNameValidation && !editMode && (
+              <div className="error-key-parent">
+                <iconError.react tag="div" className="logo-alignment-style" />
+                <div className="error-key-missing">
+                  Name must contain only letters, numbers, hyphens, and
+                  underscores
+                </div>
+              </div>
+            )}
+            {!jobNameUniqueValidation && !editMode && (
+              <div className="error-key-parent">
+                <iconError.react tag="div" className="logo-alignment-style" />
+                <div className="error-key-missing">
+                  Job name must be unique for the selected environment
+                </div>
+              </div>
+            )}
+
+            <div className="create-scheduler-form-element-input-file">
+              <Input
+                className="create-scheduler-style"
+                value={inputFileSelected}
+                Label="Input file*"
+                disabled={true}
+              />
+            </div>
+            <div className="create-scheduler-form-element">
+              <Autocomplete
+                className="create-scheduler-style"
+                options={composerList}
+                value={composerSelected}
+                onChange={(_event, val) => handleComposerSelected(val)}
+                renderInput={params => (
+                  <TextField {...params} label="Environment*" />
+                )}
+                disabled={editMode}
+              />
+            </div>
+            <div className="create-scheduler-label">Output formats</div>
+            <div className="create-scheduler-form-element">
+              <FormGroup row={true}>
+                <FormControlLabel
+                  control={
+                    <Checkbox
+                      size="small"
+                      readOnly
+                      checked={true}
+                      defaultChecked={true}
+                    />
+                  }
+                  className="create-scheduler-label-style"
+                  label={
+                    <Typography sx={{ fontSize: 13 }}>Notebook</Typography>
+                  }
+                />
+              </FormGroup>
+            </div>
+            <div className="create-scheduler-label">Parameters</div>
+            <>
+              <LabelProperties
+                labelDetail={parameterDetail}
+                setLabelDetail={setParameterDetail}
+                labelDetailUpdated={parameterDetailUpdated}
+                setLabelDetailUpdated={setParameterDetailUpdated}
+                buttonText="ADD PARAMETER"
+                keyValidation={keyValidation}
+                setKeyValidation={setKeyValidation}
+                valueValidation={valueValidation}
+                setValueValidation={setValueValidation}
+                duplicateKeyError={duplicateKeyError}
+                setDuplicateKeyError={setDuplicateKeyError}
+              />
+            </>
+            <div className="create-scheduler-form-element">
+              <FormControl>
+                <RadioGroup
+                  aria-labelledby="demo-controlled-radio-buttons-group"
+                  name="controlled-radio-buttons-group"
+                  value={selectedMode}
+                  onChange={handleSelectedModeChange}
+                  row={true}
+                >
+                  <FormControlLabel
+                    value="cluster"
+                    control={<Radio size="small" />}
+                    label={
+                      <Typography sx={{ fontSize: 13 }}>Cluster</Typography>
+                    }
+                  />
+                  <FormControlLabel
+                    value="serverless"
+                    className="create-scheduler-label-style"
+                    control={<Radio size="small" />}
+                    label={
+                      <Typography sx={{ fontSize: 13 }}>Serverless</Typography>
+                    }
+                  />
+                </RadioGroup>
+              </FormControl>
+            </div>
+            <div className="create-scheduler-form-element">
+              {selectedMode === 'cluster' && (
+                <Autocomplete
+                  className="create-scheduler-style"
+                  options={clusterList}
+                  value={clusterSelected}
+                  onChange={(_event, val) => handleClusterSelected(val)}
+                  renderInput={params => (
+                    <TextField {...params} label="Cluster*" />
+                  )}
+                />
+              )}
+              {selectedMode === 'serverless' && (
+                <Autocomplete
+                  className="create-scheduler-style"
+                  options={serverlessList}
+                  value={serverlessSelected}
+                  onChange={(_event, val) => handleServerlessSelected(val)}
+                  renderInput={params => (
+                    <TextField {...params} label="Serverless*" />
+                  )}
+                />
+              )}
+            </div>
+            {selectedMode === 'cluster' && (
+              <div className="create-scheduler-form-element">
+                <FormGroup row={true}>
+                  <FormControlLabel
+                    control={
+                      <Checkbox
+                        size="small"
+                        checked={stopCluster}
+                        onChange={handleStopCluster}
+                      />
+                    }
+                    className="create-scheduler-label-style"
+                    label={
+                      <Typography
+                        sx={{ fontSize: 13 }}
+                        title="Stopping cluster abruptly will impact if any other job is running on the cluster at the moment"
+                      >
+                        Stop the cluster after notebook execution
+                      </Typography>
+                    }
+                  />
+                </FormGroup>
+              </div>
+            )}
+            <div className="create-scheduler-form-element">
+              <Input
+                className="create-scheduler-style"
+                onChange={e => handleRetryCount(Number(e.target.value))}
+                value={retryCount}
+                Label="Retry count"
+                type="number"
+              />
+            </div>
+            <div className="create-scheduler-form-element">
+              <Input
+                className="create-scheduler-style"
+                onChange={e => handleRetryDelay(Number(e.target.value))}
+                value={retryDelay}
+                Label="Retry delay (minutes)"
+                type="number"
+              />
+            </div>
+            <div className="create-scheduler-form-element">
+              <FormGroup row={true}>
+                <FormControlLabel
+                  control={
+                    <Checkbox
+                      size="small"
+                      checked={emailOnFailure}
+                      onChange={handleFailureChange}
+                    />
+                  }
+                  className="create-scheduler-label-style"
+                  label={
+                    <Typography sx={{ fontSize: 13 }}>
+                      Email on failure
+                    </Typography>
+                  }
+                />
+                <FormControlLabel
+                  control={
+                    <Checkbox
+                      size="small"
+                      checked={emailOnRetry}
+                      onChange={handleRetryChange}
+                    />
+                  }
+                  className="create-scheduler-label-style"
+                  label={
+                    <Typography sx={{ fontSize: 13 }}>
+                      Email on retry
+                    </Typography>
+                  }
+                />
+                <FormControlLabel
+                  control={
+                    <Checkbox
+                      size="small"
+                      checked={emailOnSuccess}
+                      onChange={handleSuccessChange}
+                    />
+                  }
+                  className="create-scheduler-label-style"
+                  label={
+                    <Typography sx={{ fontSize: 13 }}>
+                      Email on success
+                    </Typography>
+                  }
+                />
+              </FormGroup>
+            </div>
+            <div className="create-scheduler-form-element">
+              {(emailOnFailure || emailOnRetry || emailOnSuccess) && (
+                <MuiChipsInput
+                  className="select-job-style"
+                  onChange={e => handleEmailList(e)}
+                  addOnBlur={true}
+                  value={emailList}
+                  inputProps={{ placeholder: '' }}
+                  label="Email recipients"
+                />
+              )}
+            </div>
+            {(emailOnFailure || emailOnRetry || emailOnSuccess) &&
+              !emailList.length && (
+                <div className="error-key-parent">
+                  <iconError.react tag="div" className="logo-alignment-style" />
+                  <div className="error-key-missing">
+                    Email recipients is required field
+                  </div>
+                </div>
+              )}
+            <div className="create-scheduler-label">Schedule</div>
+            <div className="create-scheduler-form-element">
+              <FormControl>
+                <RadioGroup
+                  aria-labelledby="demo-controlled-radio-buttons-group"
+                  name="controlled-radio-buttons-group"
+                  value={scheduleMode}
+                  onChange={handleSchedulerModeChange}
+                >
+                  <FormControlLabel
+                    value="runNow"
+                    className="create-scheduler-label-style"
+                    control={<Radio size="small" />}
+                    label={
+                      <Typography sx={{ fontSize: 13 }}>Run now</Typography>
+                    }
+                  />
+                  <FormControlLabel
+                    value="runSchedule"
+                    className="create-scheduler-label-style"
+                    control={<Radio size="small" />}
+                    label={
+                      <Typography sx={{ fontSize: 13 }}>
+                        Run on a schedule
+                      </Typography>
+                    }
+                  />
+                </RadioGroup>
+              </FormControl>
+            </div>
+            {scheduleMode === 'runSchedule' && (
+              <>
+                <div className="create-scheduler-form-element">
+                  <Cron value={scheduleValue} setValue={setScheduleValue} />
+                </div>
+                <div className="create-scheduler-form-element">
+                  <Autocomplete
+                    className="create-scheduler-style"
+                    options={timezones}
+                    value={timeZoneSelected}
+                    onChange={(_event, val) => handleTimeZoneSelected(val)}
+                    renderInput={params => (
+                      <TextField {...params} label="Time Zone" />
+                    )}
+                  />
+                </div>
+              </>
+            )}
+            <div className="save-overlay">
+              <Button
+                onClick={() => {
+                  if (!isSaveDisabled()) {
+                    handleCreateJobScheduler();
+                  }
+                }}
+                variant="contained"
+                disabled={isSaveDisabled()}
+                aria-label={editMode ? ' Update Schedule' : 'Create Schedule'}
+              >
+                <div>
+                  {editMode
+                    ? creatingScheduler
+                      ? 'UPDATING'
+                      : 'UPDATE'
+                    : creatingScheduler
+                    ? 'CREATING'
+                    : 'CREATE'}
+                </div>
+              </Button>
+              <Button
+                variant="outlined"
+                disabled={creatingScheduler}
+                aria-label="cancel Batch"
+                onClick={!creatingScheduler ? handleCancel : undefined}
+              >
+                <div>CANCEL</div>
+              </Button>
+            </div>
+          </div>
+        </div>
+      )}
+    </>
+  );
+};
+
+export default CreateNotebookScheduler;
diff --git a/src/scheduler/executionHistory.tsx b/src/scheduler/executionHistory.tsx
new file mode 100644
index 00000000..65f125b9
--- /dev/null
+++ b/src/scheduler/executionHistory.tsx
@@ -0,0 +1,219 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ *
+ * 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 React, { useEffect, useState } from 'react';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import { PickersDayProps, PickersDay } from '@mui/x-date-pickers/PickersDay';
+import { DateCalendar } from '@mui/x-date-pickers/DateCalendar';
+import dayjs, { Dayjs } from 'dayjs';
+import ListDagRuns from './listDagRuns';
+import { LabIcon } from '@jupyterlab/ui-components';
+import LeftArrowIcon from '../../style/icons/left_arrow_icon.svg';
+import ListDagTaskInstances from './listDagTaskInstances';
+
+const iconLeftArrow = new LabIcon({
+  name: 'launcher:left-arrow-icon',
+  svgstr: LeftArrowIcon
+});
+
+const ExecutionHistory = ({
+  composerName,
+  dagId,
+  handleBackButton
+}: {
+  composerName: string;
+  dagId: string;
+  handleBackButton: () => void;
+}): JSX.Element => {
+  const [dagRunId, setDagRunId] = useState('');
+  const currentDate = new Date().toLocaleDateString();
+  const [selectedDate, setSelectedDate] = useState<Dayjs | null>(null);
+  const [startDate, setStartDate] = useState('');
+  const [endDate, setEndDate] = useState('');
+
+  const [blueListDates, setBlueListDates] = useState<string[]>([]);
+  const [greyListDates, setGreyListDates] = useState<string[]>([]);
+  const [orangeListDates, setOrangeListDates] = useState<string[]>([]);
+  const [redListDates, setRedListDates] = useState<string[]>([]);
+  const [greenListDates, setGreenListDates] = useState<string[]>([]);
+  const [darkGreenListDates, setDarkGreenListDates] = useState<string[]>([]);
+
+  const handleMonthChange = () => {
+    setDagRunId('');
+    setSelectedDate(null);
+  };
+
+  const handleDateSelection = (selectedValue: any) => {
+    setDagRunId('');
+    setSelectedDate(selectedValue);
+  };
+
+  const CustomDay = (props: PickersDayProps<Dayjs>) => {
+    const { day, isFirstVisibleCell, isLastVisibleCell } = props;
+    if (isFirstVisibleCell) {
+      setStartDate(new Date(day.toDate()).toISOString());
+    }
+    if (isLastVisibleCell) {
+      const nextDate = new Date(day.toDate());
+      nextDate.setDate(day.toDate().getDate() + 1);
+      setEndDate(nextDate.toISOString());
+    }
+
+    const totalViewDates = day.date();
+    const formattedTotalViewDate = totalViewDates.toString().padStart(2, '0');
+
+    //Color codes to highlight dates in calendar
+    //Blue color code for running status
+    const isBlueExecution =
+      blueListDates.length > 0 &&
+      blueListDates.includes(formattedTotalViewDate);
+    //Grey color code for queued status
+    const isGreyExecution =
+      greyListDates.length > 0 &&
+      greyListDates.includes(formattedTotalViewDate);
+    //Orange color code for combination of failed and success status
+    const isOrangeExecution =
+      orangeListDates.length > 0 &&
+      orangeListDates.includes(formattedTotalViewDate);
+    //Red color code for only with failed status
+    const isRedExecution =
+      redListDates.length > 0 && redListDates.includes(formattedTotalViewDate);
+    //Green color code for only one with success status
+    const isGreenExecution =
+      greenListDates.length > 0 &&
+      greenListDates.includes(formattedTotalViewDate);
+    //Dark green color code for multiple with success status
+    const isDarkGreenExecution =
+      darkGreenListDates.length > 0 &&
+      darkGreenListDates.includes(formattedTotalViewDate);
+
+    const isSelectedExecution = [selectedDate?.date()].includes(totalViewDates);
+
+    return (
+      <PickersDay
+        {...props}
+        style={{
+          border: isSelectedExecution
+            ? '3px solid var(--jp-ui-font-color0)'
+            : 'none',
+          borderRadius:
+            isGreenExecution ||
+            isRedExecution ||
+            isSelectedExecution ||
+            isOrangeExecution ||
+            isGreyExecution ||
+            isBlueExecution ||
+            isDarkGreenExecution
+              ? '50%'
+              : 'none',
+          backgroundColor: isDarkGreenExecution
+            ? '#188038'
+            : isGreenExecution
+            ? '#34A853'
+            : isOrangeExecution
+            ? '#FFA500'
+            : isRedExecution
+            ? '#EA3323'
+            : isBlueExecution
+            ? '#1A73E8'
+            : isGreyExecution
+            ? '#808080'
+            : 'transparent',
+          color:
+            isGreenExecution ||
+            isRedExecution ||
+            isOrangeExecution ||
+            isGreyExecution ||
+            isBlueExecution ||
+            isDarkGreenExecution
+              ? 'white'
+              : 'inherit'
+        }}
+      />
+    );
+  };
+
+  useEffect(() => {
+    setSelectedDate(dayjs(currentDate));
+  }, []);
+
+  return (
+    <>
+      <div>
+        <div className="execution-history-header">
+          <div
+            role="button"
+            className="scheduler-back-arrow-icon"
+            onClick={() => handleBackButton()}
+          >
+            <iconLeftArrow.react
+              tag="div"
+              className="icon-white logo-alignment-style"
+            />
+          </div>
+          <div className="create-job-scheduler-title">
+            Execution History: {dagId}
+          </div>
+        </div>
+        <div className="execution-history-main-wrapper">
+          <div className="execution-history-left-wrapper">
+            <LocalizationProvider dateAdapter={AdapterDayjs}>
+              <DateCalendar
+                minDate={dayjs().year(2024).startOf('year')}
+                maxDate={dayjs(currentDate)}
+                referenceDate={dayjs(currentDate)}
+                onChange={newValue => handleDateSelection(newValue)}
+                slots={{
+                  day: CustomDay
+                }}
+                onMonthChange={() => handleMonthChange()}
+              />
+            </LocalizationProvider>
+            {startDate !== '' && endDate !== '' && (
+              <ListDagRuns
+                composerName={composerName}
+                dagId={dagId}
+                startDate={startDate}
+                endDate={endDate}
+                setDagRunId={setDagRunId}
+                selectedDate={selectedDate}
+                setBlueListDates={setBlueListDates}
+                setGreyListDates={setGreyListDates}
+                setOrangeListDates={setOrangeListDates}
+                setRedListDates={setRedListDates}
+                setGreenListDates={setGreenListDates}
+                setDarkGreenListDates={setDarkGreenListDates}
+              />
+            )}
+          </div>
+          <div className="execution-history-right-wrapper">
+            {dagRunId !== '' && (
+              <ListDagTaskInstances
+                composerName={composerName}
+                dagId={dagId}
+                dagRunId={dagRunId}
+              />
+            )}
+          </div>
+        </div>
+      </div>
+    </>
+  );
+};
+
+export default ExecutionHistory;
diff --git a/src/scheduler/listDagRuns.tsx b/src/scheduler/listDagRuns.tsx
new file mode 100644
index 00000000..c2d6d38b
--- /dev/null
+++ b/src/scheduler/listDagRuns.tsx
@@ -0,0 +1,247 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ *
+ * 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 React, { useEffect, useState } from 'react';
+import { useTable, useGlobalFilter, usePagination } from 'react-table';
+import { ClipLoader } from 'react-spinners';
+import TableData from '../utils/tableData';
+import { ICellProps } from '../utils/utils';
+import { SchedulerService } from './schedulerServices';
+import { Dayjs } from 'dayjs';
+
+interface IDagRunList {
+  dagRunId: string;
+  filteredDate: Date;
+  state: string;
+  date: Date;
+  time: string;
+}
+
+const ListDagRuns = ({
+  composerName,
+  dagId,
+  startDate,
+  endDate,
+  setDagRunId,
+  selectedDate,
+
+  setBlueListDates,
+  setGreyListDates,
+  setOrangeListDates,
+  setRedListDates,
+  setGreenListDates,
+  setDarkGreenListDates
+}: {
+  composerName: string;
+  dagId: string;
+  startDate: string;
+  endDate: string;
+  setDagRunId: (value: string) => void;
+  selectedDate: Dayjs | null;
+
+  setBlueListDates: (value: string[]) => void;
+  setGreyListDates: (value: string[]) => void;
+  setOrangeListDates: (value: string[]) => void;
+  setRedListDates: (value: string[]) => void;
+  setGreenListDates: (value: string[]) => void;
+  setDarkGreenListDates: (value: string[]) => void;
+}): JSX.Element => {
+  const [dagRunsList, setDagRunsList] = useState<IDagRunList[]>([]);
+  const [dagRunsCurrentDateList, setDagRunsCurrentDateList] = useState([]);
+  const [isLoading, setIsLoading] = useState(true);
+  const data =
+    dagRunsCurrentDateList.length > 0 ? dagRunsCurrentDateList : dagRunsList;
+  const columns = React.useMemo(
+    () => [
+      {
+        Header: 'State',
+        accessor: 'state'
+      },
+      {
+        Header: 'Date',
+        accessor: 'date'
+      },
+      {
+        Header: 'Time',
+        accessor: 'time'
+      }
+    ],
+    []
+  );
+
+  const {
+    getTableProps,
+    getTableBodyProps,
+    headerGroups,
+    rows,
+    prepareRow,
+    page
+  } = useTable(
+    //@ts-ignore react-table 'columns' which is declared here on type 'TableOptions<ICluster>'
+    { columns, data, autoResetPage: false, initialState: { pageSize: 50 } },
+    useGlobalFilter,
+    usePagination
+  );
+
+  const tableDataCondition = (cell: ICellProps) => {
+    if (cell.column.Header === 'State') {
+      if (cell.value === 'success') {
+        return (
+          <div className="dag-run-state-parent">
+            <td
+              {...cell.getCellProps()}
+              className="dag-runs-table-data-state-success"
+            >
+              {cell.render('Cell')}
+            </td>
+          </div>
+        );
+      } else if (cell.value === 'failed') {
+        return (
+          <div className="dag-run-state-parent">
+            <td
+              {...cell.getCellProps()}
+              className="dag-runs-table-data-state-failure"
+            >
+              {cell.render('Cell')}
+            </td>
+          </div>
+        );
+      } else if (cell.value === 'running') {
+        return (
+          <div className="dag-run-state-parent">
+            <td
+              {...cell.getCellProps()}
+              className="dag-runs-table-data-state-running"
+            >
+              {cell.render('Cell')}
+            </td>
+          </div>
+        );
+      } else if (cell.value === 'queued') {
+        return (
+          <div className="dag-run-state-parent">
+            <td
+              {...cell.getCellProps()}
+              className="dag-runs-table-data-state-queued"
+            >
+              {cell.render('Cell')}
+            </td>
+          </div>
+        );
+      }
+    }
+    return (
+      <td {...cell.getCellProps()} className="notebook-template-table-data">
+        {cell.render('Cell')}
+      </td>
+    );
+  };
+
+  const listDagRunsList = async () => {
+    await SchedulerService.listDagRunsListService(
+      composerName,
+      dagId,
+      startDate,
+      endDate,
+      setDagRunsList,
+      setDagRunId,
+      setIsLoading,
+      setBlueListDates,
+      setGreyListDates,
+      setOrangeListDates,
+      setRedListDates,
+      setGreenListDates,
+      setDarkGreenListDates
+    );
+  };
+
+  useEffect(() => {
+    listDagRunsList();
+  }, [startDate, endDate]);
+
+  useEffect(() => {
+    let currentDate = selectedDate
+      ? new Date(selectedDate.toDate()).toDateString()
+      : null;
+    let currentDateDagRunList: any = dagRunsList.filter((dagRun: any) => {
+      return dagRun.date === currentDate;
+    });
+    if (currentDateDagRunList.length > 0) {
+      setDagRunsCurrentDateList(currentDateDagRunList);
+      setDagRunId(
+        currentDateDagRunList[currentDateDagRunList.length - 1].dagRunId
+      );
+    } else {
+      setDagRunsCurrentDateList([]);
+      setDagRunId('');
+    }
+  }, [selectedDate, dagRunsList]);
+
+  return (
+    <div>
+      <>
+        {(dagRunsList.length > 0 && selectedDate === null) ||
+        (selectedDate !== null && dagRunsCurrentDateList.length > 0) ? (
+          <div>
+            <div className="dag-runs-list-table-parent">
+              <TableData
+                getTableProps={getTableProps}
+                headerGroups={headerGroups}
+                getTableBodyProps={getTableBodyProps}
+                isLoading={isLoading}
+                rows={rows}
+                page={page}
+                prepareRow={prepareRow}
+                tableDataCondition={tableDataCondition}
+                fromPage="Dag Runs"
+                setDagRunId={setDagRunId}
+                selectedDagIndex={
+                  dagRunsCurrentDateList.length > 0
+                    ? dagRunsCurrentDateList.length - 1
+                    : dagRunsList.length > 0
+                    ? dagRunsList.length - 1
+                    : -1
+                }
+              />
+            </div>
+          </div>
+        ) : (
+          <div>
+            {isLoading && (
+              <div className="spin-loader-main">
+                <ClipLoader
+                  color="#3367d6"
+                  loading={true}
+                  size={18}
+                  aria-label="Loading Spinner"
+                  data-testid="loader"
+                />
+                Loading Dag Runs
+              </div>
+            )}
+            {!isLoading && (
+              <div className="no-data-style">No rows to display</div>
+            )}
+          </div>
+        )}
+      </>
+    </div>
+  );
+};
+
+export default ListDagRuns;
diff --git a/src/scheduler/listDagTaskInstances.tsx b/src/scheduler/listDagTaskInstances.tsx
new file mode 100644
index 00000000..5c6a4e21
--- /dev/null
+++ b/src/scheduler/listDagTaskInstances.tsx
@@ -0,0 +1,224 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ *
+ * 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 React, { useEffect, useState } from 'react';
+import { Typography, CircularProgress, IconButton } from '@mui/material';
+import { SchedulerService } from './schedulerServices';
+import { LabIcon } from '@jupyterlab/ui-components';
+import dagTaskSuccessIcon from '../../style/icons/dag_task_success_icon.svg';
+import dagTaskFailedIcon from '../../style/icons/dag_task_failed_icon.svg';
+import stopIcon from '../../style/icons/stop_icon.svg';
+import expandLessIcon from '../../style/icons/expand_less.svg';
+import expandMoreIcon from '../../style/icons/expand_more.svg';
+
+const iconDagTaskFailed = new LabIcon({
+  name: 'launcher:dag-task-failed-icon',
+  svgstr: dagTaskFailedIcon
+});
+const iconDagTaskSuccess = new LabIcon({
+  name: 'launcher:dag-task-success-icon',
+  svgstr: dagTaskSuccessIcon
+});
+const iconStop = new LabIcon({
+  name: 'launcher:stop-icon',
+  svgstr: stopIcon
+});
+const iconExpandLess = new LabIcon({
+  name: 'launcher:expand-less-icon',
+  svgstr: expandLessIcon
+});
+const iconExpandMore = new LabIcon({
+  name: 'launcher:expand-more-icon',
+  svgstr: expandMoreIcon
+});
+
+const ListDagTaskInstances = ({
+  composerName,
+  dagId,
+  dagRunId
+}: {
+  composerName: string;
+  dagId: string;
+  dagRunId: string;
+}): JSX.Element => {
+  const [dagTaskInstancesList, setDagTaskInstancesList] = useState<any>([]);
+  const [isLoading, setIsLoading] = useState(true);
+  const [isLoadingLogs, setIsLoadingLogs] = useState(false);
+  const [expanded, setExpanded] = useState<string | false>(false);
+  const [loglist, setLogList] = useState('');
+
+  const listDagTaskInstancesRunsList = async () => {
+    await SchedulerService.listDagTaskInstancesListService(
+      composerName,
+      dagId,
+      dagRunId,
+      setDagTaskInstancesList,
+      setIsLoading
+    );
+  };
+
+  useEffect(() => {
+    listDagTaskInstancesRunsList();
+    setExpanded(false);
+  }, [dagRunId]);
+
+  useEffect(() => {
+    if (dagTaskInstancesList.length > 0) {
+      setExpanded('0');
+      listDagTaskLogList('0', dagTaskInstancesList[0].tryNumber);
+    }
+  }, [dagTaskInstancesList]);
+
+  const handleChange = (
+    index: string,
+    iconIndex: number,
+    fromClick: string
+  ) => {
+    if (`${index}` === expanded && fromClick === 'expandClick') {
+      setExpanded(false);
+    } else {
+      setExpanded(`${index}`);
+      listDagTaskLogList(index, iconIndex);
+    }
+  };
+
+  const listDagTaskLogList = async (index: string, iconIndex: number) => {
+    await SchedulerService.listDagTaskLogsListService(
+      composerName,
+      dagId,
+      dagRunId,
+      dagTaskInstancesList[index].taskId,
+      iconIndex,
+      setLogList,
+      setIsLoadingLogs
+    );
+  };
+  return (
+    <div>
+      {dagTaskInstancesList.length > 0 ? (
+        <div>
+          <div className="accordion-row-parent-header">
+            <div className="accordion-row-data">Task Id</div>
+            <div className="accordion-row-data">Attempts</div>
+            <div className="accordion-row-data">Duration (in seconds)</div>
+            <div className="accordion-row-data-expand-logo"></div>
+          </div>
+          {dagTaskInstancesList.length > 0 &&
+            dagTaskInstancesList.map((taskInstance: any, index: string) => (
+              <div>
+                <div className="accordion-row-parent">
+                  <div className="accordion-row-data">
+                    {taskInstance.taskId}
+                  </div>
+                  <div className="accordion-row-data">
+                    {taskInstance.tryNumber === 0 ? (
+                      <iconStop.react
+                        tag="div"
+                        className="logo-alignment-style-accordion"
+                      />
+                    ) : (
+                      <div className="logo-row-container">
+                        {Array.from({ length: taskInstance.tryNumber }).map(
+                          (_, i) => (
+                            <IconButton
+                              key={i}
+                              className="logo-alignment-style-accordion"
+                              onClick={() =>
+                                handleChange(index, i + 1, 'attemptsClick')
+                              }
+                            >
+                              {i === taskInstance.tryNumber - 1 ? (
+                                taskInstance.state === 'failed' ? (
+                                  <iconDagTaskFailed.react tag="div" />
+                                ) : (
+                                  <iconDagTaskSuccess.react tag="div" />
+                                )
+                              ) : (
+                                <iconDagTaskFailed.react tag="div" />
+                              )}
+                            </IconButton>
+                          )
+                        )}
+                      </div>
+                    )}
+                  </div>
+                  <div className="accordion-row-data">
+                    {taskInstance.duration}
+                  </div>
+                  {taskInstance.tryNumber !== 0 ? (
+                    <div
+                      className="accordion-row-data-expand-logo"
+                      onClick={() =>
+                        handleChange(
+                          index,
+                          taskInstance.tryNumber,
+                          'expandClick'
+                        )
+                      }
+                    >
+                      {expanded === `${index}` ? (
+                        <iconExpandLess.react
+                          tag="div"
+                          className="icon-white logo-alignment-style-accordion"
+                        />
+                      ) : (
+                        <iconExpandMore.react
+                          tag="div"
+                          className="icon-white logo-alignment-style-accordion"
+                        />
+                      )}
+                    </div>
+                  ) : (
+                    <div className="accordion-row-data-expand-logo"></div>
+                  )}
+                </div>
+
+                {isLoadingLogs && expanded === `${index}` ? (
+                  <div className="spin-loader-main">
+                    <CircularProgress color="primary" size={18} />
+                    Loading Dag Runs Task Logs
+                  </div>
+                ) : (
+                  expanded === `${index}` && (
+                    <div>
+                      {' '}
+                      <Typography>
+                        <pre className="logs-content-style">{loglist}</pre>
+                      </Typography>{' '}
+                    </div>
+                  )
+                )}
+              </div>
+            ))}
+        </div>
+      ) : (
+        <div>
+          {isLoading ? (
+            <div className="spin-loader-main">
+              <CircularProgress color="primary" size={18} />
+              Loading Dag Runs Task Instances
+            </div>
+          ) : (
+            <div className="no-data-style">No rows to display</div>
+          )}
+        </div>
+      )}
+    </div>
+  );
+};
+
+export default ListDagTaskInstances;
diff --git a/src/scheduler/listNotebookScheduler.tsx b/src/scheduler/listNotebookScheduler.tsx
new file mode 100644
index 00000000..f844912e
--- /dev/null
+++ b/src/scheduler/listNotebookScheduler.tsx
@@ -0,0 +1,559 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ *
+ * 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 React, { useState, useEffect, useRef } from 'react';
+import { useTable, usePagination } from 'react-table';
+import TableData from '../utils/tableData';
+import { PaginationView } from '../utils/paginationView';
+import { ICellProps } from '../utils/utils';
+import { JupyterFrontEnd } from '@jupyterlab/application';
+import { Autocomplete, TextField } from '@mui/material';
+import deleteIcon from '../../style/icons/delete_icon.svg';
+import { LabIcon } from '@jupyterlab/ui-components';
+import playIcon from '../../style/icons/play_icon.svg';
+import pauseIcon from '../../style/icons/pause_icon.svg';
+import downloadIcon from '../../style/icons/download_icon.svg';
+import EditIconDisable from '../../style/icons/edit_icon_disable.svg';
+import EditNotebookIcon from '../../style/icons/edit_notebook_icon.svg';
+import { SchedulerService } from './schedulerServices';
+import { ClipLoader } from 'react-spinners';
+import DeletePopup from '../utils/deletePopup';
+import PollingTimer from '../utils/pollingTimer';
+import { scheduleMode } from '../utils/const';
+
+const iconDelete = new LabIcon({
+  name: 'launcher:delete-icon',
+  svgstr: deleteIcon
+});
+const iconPlay = new LabIcon({
+  name: 'launcher:play-icon',
+  svgstr: playIcon
+});
+const iconPause = new LabIcon({
+  name: 'launcher:pause-icon',
+  svgstr: pauseIcon
+});
+
+const iconDownload = new LabIcon({
+  name: 'launcher:download-icon',
+  svgstr: downloadIcon
+});
+const iconEditDag = new LabIcon({
+  name: 'launcher:edit-disable-icon',
+  svgstr: EditIconDisable
+});
+const iconEditNotebook = new LabIcon({
+  name: 'launcher:edit-notebook-icon',
+  svgstr: EditNotebookIcon
+});
+
+interface IDagList {
+  jobid: string;
+  notebookname: string;
+  schedule: string;
+  scheduleInterval: string;
+}
+
+function listNotebookScheduler({
+  app,
+  handleDagIdSelection,
+  backButtonComposerName,
+  composerSelectedFromCreate,
+  setCreateCompleted,
+  setJobNameSelected,
+  setComposerSelected,
+  setScheduleMode,
+  setScheduleValue,
+
+  setInputFileSelected,
+  setParameterDetail,
+  setParameterDetailUpdated,
+  setSelectedMode,
+  setClusterSelected,
+  setServerlessSelected,
+  setServerlessDataSelected,
+  serverlessDataList,
+  setServerlessDataList,
+  setServerlessList,
+  setRetryCount,
+  setRetryDelay,
+  setEmailOnFailure,
+  setEmailonRetry,
+  setEmailOnSuccess,
+  setEmailList,
+  setStopCluster,
+  setTimeZoneSelected,
+  setEditMode
+}: {
+  app: JupyterFrontEnd;
+  handleDagIdSelection: (composerName: string, dagId: string) => void;
+  backButtonComposerName: string;
+  composerSelectedFromCreate: string;
+  setCreateCompleted?: (value: boolean) => void;
+  setJobNameSelected?: (value: string) => void;
+  setComposerSelected?: (value: string) => void;
+  setScheduleMode?: (value: scheduleMode) => void;
+  setScheduleValue?: (value: string) => void;
+
+  setInputFileSelected?: (value: string) => void;
+  setParameterDetail?: (value: string[]) => void;
+  setParameterDetailUpdated?: (value: string[]) => void;
+  setSelectedMode?: (value: string) => void;
+  setClusterSelected?: (value: string) => void;
+  setServerlessSelected?: (value: string) => void;
+  setServerlessDataSelected?: (value: {}) => void;
+  serverlessDataList?: string[];
+  setServerlessDataList?: (value: string[]) => void;
+  setServerlessList?: (value: string[]) => void;
+  setRetryCount?: (value: number) => void;
+  setRetryDelay?: (value: number) => void;
+  setEmailOnFailure?: (value: boolean) => void;
+  setEmailonRetry?: (value: boolean) => void;
+  setEmailOnSuccess?: (value: boolean) => void;
+  setEmailList?: (value: string[]) => void;
+  setStopCluster?: (value: boolean) => void;
+  setTimeZoneSelected?: (value: string) => void;
+  setEditMode?: (value: boolean) => void;
+}) {
+  const [isLoading, setIsLoading] = useState(true);
+  const [composerList, setComposerList] = useState<string[]>([]);
+  const [composerSelectedList, setComposerSelectedList] = useState('');
+  const [dagList, setDagList] = useState<IDagList[]>([]);
+  const data = dagList;
+  const [bucketName, setBucketName] = useState('');
+  const backselectedEnvironment = backButtonComposerName;
+  const createSelectedEnvironment = composerSelectedFromCreate;
+  const [deletePopupOpen, setDeletePopupOpen] = useState(false);
+  const [selectedDagId, setSelectedDagId] = useState('');
+  const [editDagLoading, setEditDagLoading] = useState('');
+  const [pollingDisable] = useState(false);
+  const [inputNotebookFilePath, setInputNotebookFilePath] = useState('');
+  const [editNotebookLoading, setEditNotebookLoading] = useState('');
+  const [deletingNotebook, setDeletingNotebook] = useState(false);
+  const columns = React.useMemo(
+    () => [
+      {
+        Header: 'Job Name',
+        accessor: 'jobid'
+      },
+      {
+        Header: 'Schedule',
+        accessor: 'schedule'
+      },
+      {
+        Header: 'Status',
+        accessor: 'status'
+      },
+      {
+        Header: 'Actions',
+        accessor: 'actions'
+      }
+    ],
+    []
+  );
+  const timer = useRef<NodeJS.Timeout | undefined>(undefined);
+  const pollingDagList = async (
+    pollingFunction: () => void,
+    pollingDisable: boolean
+  ) => {
+    timer.current = PollingTimer(
+      pollingFunction,
+      pollingDisable,
+      timer.current
+    );
+  };
+  const handleComposerSelected = (data: string | null) => {
+    if (data) {
+      const selectedComposer = data.toString();
+      setComposerSelectedList(selectedComposer);
+    }
+  };
+  const handleUpdateScheduler = async (
+    dag_id: string,
+    is_status_paused: boolean
+  ) => {
+    await SchedulerService.handleUpdateSchedulerAPIService(
+      composerSelectedList,
+      dag_id,
+      is_status_paused,
+      setDagList,
+      setIsLoading,
+      setBucketName
+    );
+  };
+  const handleDeletePopUp = (dag_id: string) => {
+    setSelectedDagId(dag_id);
+    setDeletePopupOpen(true);
+  };
+  const handleEditNotebook = async (event: React.MouseEvent) => {
+    const jobid = event.currentTarget.getAttribute('data-jobid');
+    if (jobid !== null) {
+      await SchedulerService.editNotebookSchedulerService(
+        bucketName,
+        jobid,
+        setInputNotebookFilePath,
+        setEditNotebookLoading
+      );
+    }
+  };
+  const handleEditDags = async (event: React.MouseEvent) => {
+    const jobid = event.currentTarget.getAttribute('data-jobid');
+    if (jobid !== null) {
+      await SchedulerService.editJobSchedulerService(
+        bucketName,
+        jobid,
+        composerSelectedList,
+        setEditDagLoading,
+        setCreateCompleted,
+        setJobNameSelected,
+        setComposerSelected,
+        setScheduleMode,
+        setScheduleValue,
+
+        setInputFileSelected,
+        setParameterDetail,
+        setParameterDetailUpdated,
+        setSelectedMode,
+        setClusterSelected,
+        setServerlessSelected,
+        setServerlessDataSelected,
+        serverlessDataList,
+        setServerlessDataList,
+        setServerlessList,
+        setRetryCount,
+        setRetryDelay,
+        setEmailOnFailure,
+        setEmailonRetry,
+        setEmailOnSuccess,
+        setEmailList,
+        setStopCluster,
+        setTimeZoneSelected,
+        setEditMode
+      );
+    }
+  };
+
+  const handleCancelDelete = () => {
+    setDeletePopupOpen(false);
+  };
+
+  const handleDeleteScheduler = async () => {
+    setDeletingNotebook(true);
+    await SchedulerService.handleDeleteSchedulerAPIService(
+      composerSelectedList,
+      selectedDagId,
+      setDagList,
+      setIsLoading,
+      setBucketName
+    );
+    setDeletePopupOpen(false);
+    setDeletingNotebook(false);
+  };
+
+  const handleDownloadScheduler = async (event: React.MouseEvent) => {
+    const jobid = event.currentTarget.getAttribute('data-jobid')!;
+    await SchedulerService.handleDownloadSchedulerAPIService(
+      composerSelectedList,
+      jobid,
+      bucketName
+    );
+  };
+
+  const listComposersAPI = async () => {
+    await SchedulerService.listComposersAPIService(
+      setComposerList,
+      setIsLoading
+    );
+  };
+
+  const listDagInfoAPI = async () => {
+    await SchedulerService.listDagInfoAPIService(
+      setDagList,
+      setIsLoading,
+      setBucketName,
+      composerSelectedList
+    );
+  };
+
+  const {
+    getTableProps,
+    getTableBodyProps,
+    headerGroups,
+    rows,
+    prepareRow,
+    page,
+    canPreviousPage,
+    canNextPage,
+    nextPage,
+    previousPage,
+    setPageSize,
+    state: { pageIndex, pageSize }
+  } = useTable(
+    //@ts-ignore react-table 'columns' which is declared here on type 'TableOptions<ICluster>'
+    { columns, data, autoResetPage: false, initialState: { pageSize: 50 } },
+    usePagination
+  );
+
+  const renderActions = (data: any) => {
+    const is_status_paused = data.status === 'Paused';
+    return (
+      <div className="actions-icon">
+        <div
+          role="button"
+          className="icon-buttons-style"
+          title={is_status_paused ? 'Unpause' : 'Pause'}
+          onClick={e => handleUpdateScheduler(data.jobid, is_status_paused)}
+        >
+          {is_status_paused ? (
+            <iconPlay.react
+              tag="div"
+              className="icon-white logo-alignment-style"
+            />
+          ) : (
+            <iconPause.react
+              tag="div"
+              className="icon-white logo-alignment-style"
+            />
+          )}
+        </div>
+        <div
+          role="button"
+          className="icon-buttons-style"
+          title="Download Notebook"
+          data-jobid={data.jobid}
+          onClick={e => handleDownloadScheduler(e)}
+        >
+          <iconDownload.react
+            tag="div"
+            className="icon-white logo-alignment-style"
+          />
+        </div>
+        {data.jobid === editDagLoading ? (
+          <div className="icon-buttons-style">
+            <ClipLoader
+              color="#3367d6"
+              loading={true}
+              size={18}
+              aria-label="Loading Spinner"
+              data-testid="loader"
+            />
+          </div>
+        ) : (
+          <div
+            role="button"
+            className="icon-buttons-style"
+            title="Edit Schedule"
+            data-jobid={data.jobid}
+            onClick={e => handleEditDags(e)}
+          >
+            <iconEditDag.react
+              tag="div"
+              className="icon-white logo-alignment-style"
+            />
+          </div>
+        )}
+        {data.jobid === editNotebookLoading ? (
+          <div className="icon-buttons-style">
+            <ClipLoader
+              color="#3367d6"
+              loading={true}
+              size={18}
+              aria-label="Loading Spinner"
+              data-testid="loader"
+            />
+          </div>
+        ) : (
+          <div
+            role="button"
+            className="icon-buttons-style"
+            title="Edit Notebook"
+            data-jobid={data.jobid}
+            onClick={e => handleEditNotebook(e)}
+          >
+            <iconEditNotebook.react
+              tag="div"
+              className="icon-white logo-alignment-style"
+            />
+          </div>
+        )}
+        <div
+          role="button"
+          className="icon-buttons-style"
+          title="Delete"
+          onClick={() => handleDeletePopUp(data.jobid)}
+        >
+          <iconDelete.react
+            tag="div"
+            className="icon-white logo-alignment-style"
+          />
+        </div>
+      </div>
+    );
+  };
+
+  const tableDataCondition = (cell: ICellProps) => {
+    if (cell.column.Header === 'Actions') {
+      return (
+        <td {...cell.getCellProps()} className="clusters-table-data">
+          {renderActions(cell.row.original)}
+        </td>
+      );
+    } else if (cell.column.Header === 'Job Name') {
+      return (
+        <td
+          {...cell.getCellProps()}
+          className="clusters-table-data"
+          onClick={() => handleDagIdSelection(composerSelectedList, cell.value)}
+        >
+          {cell.value}
+        </td>
+      );
+    } else {
+      return (
+        <td {...cell.getCellProps()} className="clusters-table-data">
+          {cell.render('Cell')}
+        </td>
+      );
+    }
+  };
+  useEffect(() => {
+    if (inputNotebookFilePath !== '') {
+      let filePath = inputNotebookFilePath.replace('gs://', 'gs:');
+      app.commands.execute('docmanager:open', {
+        path: filePath
+      });
+      setInputNotebookFilePath('');
+    }
+  }, [inputNotebookFilePath]);
+
+  useEffect(() => {
+    const loadComposerListAndSelectFirst = async () => {
+      await listComposersAPI();
+    };
+    loadComposerListAndSelectFirst();
+  }, []);
+
+  useEffect(() => {
+    if (
+      composerList.length > 0 &&
+      backselectedEnvironment === '' &&
+      createSelectedEnvironment === ''
+    ) {
+      setComposerSelectedList(composerList[0]);
+    }
+    if (
+      composerList.length > 0 &&
+      backselectedEnvironment === '' &&
+      createSelectedEnvironment !== ''
+    ) {
+      setComposerSelectedList(createSelectedEnvironment);
+    }
+    if (composerList.length > 0 && backselectedEnvironment !== '') {
+      setComposerSelectedList(backselectedEnvironment);
+    }
+  }, [composerList]);
+
+  useEffect(() => {
+    if (composerSelectedList !== '') {
+      setIsLoading(true);
+      listDagInfoAPI();
+    }
+  }, [composerSelectedList]);
+
+  useEffect(() => {
+    if (composerSelectedList !== '') {
+      pollingDagList(listDagInfoAPI, pollingDisable);
+    }
+    return () => {
+      pollingDagList(listDagInfoAPI, true);
+    };
+  }, [composerSelectedList]);
+
+  return (
+    <div>
+      <div className="select-text-overlay-scheduler">
+        <div className="create-scheduler-form-element">
+          <Autocomplete
+            options={composerList}
+            value={composerSelectedList}
+            onChange={(_event, val) => {
+              handleComposerSelected(val);
+            }}
+            renderInput={params => (
+              <TextField {...params} label="Environment*" />
+            )}
+          />
+        </div>
+      </div>
+      {dagList.length > 0 ? (
+        <div className="notebook-templates-list-table-parent">
+          <TableData
+            getTableProps={getTableProps}
+            headerGroups={headerGroups}
+            getTableBodyProps={getTableBodyProps}
+            isLoading={isLoading}
+            rows={rows}
+            page={page}
+            prepareRow={prepareRow}
+            tableDataCondition={tableDataCondition}
+            fromPage="Notebook Schedulers"
+          />
+          {dagList.length > 50 && (
+            <PaginationView
+              pageSize={pageSize}
+              setPageSize={setPageSize}
+              pageIndex={pageIndex}
+              allData={dagList}
+              previousPage={previousPage}
+              nextPage={nextPage}
+              canPreviousPage={canPreviousPage}
+              canNextPage={canNextPage}
+            />
+          )}
+          {deletePopupOpen && (
+            <DeletePopup
+              onCancel={() => handleCancelDelete()}
+              onDelete={() => handleDeleteScheduler()}
+              deletePopupOpen={deletePopupOpen}
+              DeleteMsg={`This will delete ${selectedDagId} and cannot be undone.`}
+              deletingNotebook={deletingNotebook}
+            />
+          )}
+        </div>
+      ) : (
+        <div>
+          {isLoading && (
+            <div className="spin-loader-main">
+              <ClipLoader
+                color="#3367d6"
+                loading={true}
+                size={18}
+                aria-label="Loading Spinner"
+                data-testid="loader"
+              />
+              Loading Notebook Schedulers
+            </div>
+          )}
+          {!isLoading && (
+            <div className="no-data-style">No rows to display</div>
+          )}
+        </div>
+      )}
+    </div>
+  );
+}
+export default listNotebookScheduler;
diff --git a/src/scheduler/notebookJobs.tsx b/src/scheduler/notebookJobs.tsx
new file mode 100644
index 00000000..16006677
--- /dev/null
+++ b/src/scheduler/notebookJobs.tsx
@@ -0,0 +1,174 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ *
+ * 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 React, { useState } from 'react';
+import { DataprocWidget } from '../controls/DataprocWidget';
+import { JupyterLab } from '@jupyterlab/application';
+import { IThemeManager } from '@jupyterlab/apputils';
+import ListNotebookScheduler from './listNotebookScheduler';
+import ExecutionHistory from './executionHistory';
+import { scheduleMode } from '../utils/const';
+
+const NotebookJobComponent = ({
+  app,
+  composerSelectedFromCreate,
+  setCreateCompleted,
+  setJobNameSelected,
+  setComposerSelected,
+  setScheduleMode,
+  setScheduleValue,
+
+  setInputFileSelected,
+  setParameterDetail,
+  setParameterDetailUpdated,
+  setSelectedMode,
+  setClusterSelected,
+  setServerlessSelected,
+  setServerlessDataSelected,
+  serverlessDataList,
+  setServerlessDataList,
+  setServerlessList,
+  setRetryCount,
+  setRetryDelay,
+  setEmailOnFailure,
+  setEmailonRetry,
+  setEmailOnSuccess,
+  setEmailList,
+  setStopCluster,
+  setTimeZoneSelected,
+  setEditMode
+}: {
+  app: JupyterLab;
+  themeManager: IThemeManager;
+  composerSelectedFromCreate: string;
+  setCreateCompleted?: (value: boolean) => void;
+  setJobNameSelected?: (value: string) => void;
+  setComposerSelected?: (value: string) => void;
+  setScheduleMode?: (value: scheduleMode) => void;
+  setScheduleValue?: (value: string) => void;
+
+  setInputFileSelected?: (value: string) => void;
+  setParameterDetail?: (value: string[]) => void;
+  setParameterDetailUpdated?: (value: string[]) => void;
+  setSelectedMode?: (value: string) => void;
+  setClusterSelected?: (value: string) => void;
+  setServerlessSelected?: (value: string) => void;
+  setServerlessDataSelected?: (value: {}) => void;
+  serverlessDataList?: any;
+  setServerlessDataList?: (value: string[]) => void;
+  setServerlessList?: (value: string[]) => void;
+  setRetryCount?: (value: number) => void;
+  setRetryDelay?: (value: number) => void;
+  setEmailOnFailure?: (value: boolean) => void;
+  setEmailonRetry?: (value: boolean) => void;
+  setEmailOnSuccess?: (value: boolean) => void;
+  setEmailList?: (value: string[]) => void;
+  setStopCluster?: (value: boolean) => void;
+  setTimeZoneSelected?: (value: string) => void;
+  setEditMode?: (value: boolean) => void;
+}): React.JSX.Element => {
+  const [showExecutionHistory, setShowExecutionHistory] = useState(false);
+  const [composerName, setComposerName] = useState('');
+  const [dagId, setDagId] = useState('');
+  const [backComposerName, setBackComposerName] = useState('');
+  const handleDagIdSelection = (composerName: string, dagId: string) => {
+    setShowExecutionHistory(true);
+    setComposerName(composerName);
+    setDagId(dagId);
+  };
+
+  const handleBackButton = () => {
+    setShowExecutionHistory(false);
+    setBackComposerName(composerName);
+  };
+
+  return (
+    <div className="component-level">
+      {showExecutionHistory ? (
+        <ExecutionHistory
+          composerName={composerName}
+          dagId={dagId}
+          handleBackButton={handleBackButton}
+        />
+      ) : (
+        <div>
+          <div className="clusters-list-overlay" role="tab">
+            <div className="cluster-details-title">Scheduled Jobs</div>
+          </div>
+          <div>
+            <ListNotebookScheduler
+              app={app}
+              handleDagIdSelection={handleDagIdSelection}
+              backButtonComposerName={backComposerName}
+              composerSelectedFromCreate={composerSelectedFromCreate}
+              setCreateCompleted={setCreateCompleted}
+              setJobNameSelected={setJobNameSelected}
+              setComposerSelected={setComposerSelected}
+              setScheduleMode={setScheduleMode}
+              setScheduleValue={setScheduleValue}
+              setInputFileSelected={setInputFileSelected}
+              setParameterDetail={setParameterDetail}
+              setParameterDetailUpdated={setParameterDetailUpdated}
+              setSelectedMode={setSelectedMode}
+              setClusterSelected={setClusterSelected}
+              setServerlessSelected={setServerlessSelected}
+              setServerlessDataSelected={setServerlessDataSelected}
+              serverlessDataList={serverlessDataList}
+              setServerlessDataList={setServerlessDataList}
+              setServerlessList={setServerlessList}
+              setRetryCount={setRetryCount}
+              setRetryDelay={setRetryDelay}
+              setEmailOnFailure={setEmailOnFailure}
+              setEmailonRetry={setEmailonRetry}
+              setEmailOnSuccess={setEmailOnSuccess}
+              setEmailList={setEmailList}
+              setStopCluster={setStopCluster}
+              setTimeZoneSelected={setTimeZoneSelected}
+              setEditMode={setEditMode}
+            />
+          </div>
+        </div>
+      )}
+    </div>
+  );
+};
+
+export class NotebookJobs extends DataprocWidget {
+  app: JupyterLab;
+  composerSelectedFromCreate: string;
+
+  constructor(
+    app: JupyterLab,
+    themeManager: IThemeManager,
+    composerSelectedFromCreate: string
+  ) {
+    super(themeManager);
+    this.app = app;
+    this.composerSelectedFromCreate = composerSelectedFromCreate;
+  }
+  renderInternal(): React.JSX.Element {
+    return (
+      <NotebookJobComponent
+        app={this.app}
+        themeManager={this.themeManager}
+        composerSelectedFromCreate={this.composerSelectedFromCreate}
+      />
+    );
+  }
+}
+
+export default NotebookJobComponent;
diff --git a/src/scheduler/notebookScheduler.tsx b/src/scheduler/notebookScheduler.tsx
new file mode 100644
index 00000000..645b80cb
--- /dev/null
+++ b/src/scheduler/notebookScheduler.tsx
@@ -0,0 +1,71 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ *
+ * 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 React from 'react';
+
+import { IThemeManager } from '@jupyterlab/apputils';
+import { DataprocWidget } from '../controls/DataprocWidget';
+import { JupyterLab } from '@jupyterlab/application';
+import CreateNotebookScheduler from './createNotebookScheduler';
+
+import { DocumentRegistry } from '@jupyterlab/docregistry';
+import { INotebookModel } from '@jupyterlab/notebook';
+
+const NotebookSchedulerComponent = ({
+  themeManager,
+  app,
+  context
+}: {
+  themeManager: IThemeManager;
+  app: JupyterLab;
+  context: DocumentRegistry.IContext<INotebookModel> | string;
+}): JSX.Element => {
+  return (
+    <div className="component-level">
+      <CreateNotebookScheduler
+        themeManager={themeManager}
+        app={app}
+        context={context}
+      />
+    </div>
+  );
+};
+
+export class NotebookScheduler extends DataprocWidget {
+  app: JupyterLab;
+  context: DocumentRegistry.IContext<INotebookModel> | string;
+
+  constructor(
+    app: JupyterLab,
+    themeManager: IThemeManager,
+    context: DocumentRegistry.IContext<INotebookModel> | string
+  ) {
+    super(themeManager);
+    this.app = app;
+    this.context = context;
+  }
+
+  renderInternal(): React.JSX.Element {
+    return (
+      <NotebookSchedulerComponent
+        themeManager={this.themeManager}
+        app={this.app}
+        context={this.context}
+      />
+    );
+  }
+}
diff --git a/src/scheduler/schedulerServices.tsx b/src/scheduler/schedulerServices.tsx
new file mode 100644
index 00000000..9a538db7
--- /dev/null
+++ b/src/scheduler/schedulerServices.tsx
@@ -0,0 +1,733 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ *
+ * 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 { toast } from 'react-toastify';
+import { requestAPI } from '../handler/handler';
+import { DataprocLoggingService, LOG_LEVEL } from '../utils/loggingService';
+import { toastifyCustomStyle } from '../utils/utils';
+import { JupyterLab } from '@jupyterlab/application';
+import { scheduleMode } from '../utils/const';
+
+interface IPayload {
+  input_filename: string;
+  composer_environment_name: string;
+  output_formats: string[];
+  parameters: string[];
+  cluster_name?: string;
+  serverless_name?: {} | undefined;
+  mode_selected: string;
+  retry_count: number | undefined;
+  retry_delay: number | undefined;
+  email_failure: boolean;
+  email_delay: boolean;
+  email: string[];
+  name: string;
+  schedule_value: string;
+  stop_cluster: boolean;
+  time_zone?: string;
+  dag_id: string;
+}
+
+interface IUpdateSchedulerAPIResponse {
+  status: number;
+  error: string;
+}
+
+interface ISchedulerDagData {
+  dag_id: string;
+  timetable_description: string;
+  is_paused: string;
+  schedule_interval: null | {
+    value: string;
+  };
+}
+interface IDagList {
+  jobid: string;
+  notebookname: string;
+  schedule: string;
+  scheduleInterval: string;
+}
+
+interface IClusterAPIResponse {
+  clusterName: string;
+  clusterUuid: string;
+  config: {};
+  labels: {};
+  metrics: {};
+  projectId: string;
+  status: {};
+  statusHistory: [];
+}
+
+interface IComposerAPIResponse {
+  name: string;
+  label: string;
+  description: string;
+  file_extensions: [];
+  metadata: {};
+}
+
+interface IDagRunList {
+  dagRunId: string;
+  filteredDate: Date;
+  state: string;
+  date: Date;
+  time: string;
+}
+export class SchedulerService {
+  static listClustersAPIService = async (
+    setClusterList: (value: string[]) => void,
+    nextPageToken?: string,
+    previousClustersList?: (value: string[]) => void
+  ) => {
+    const pageToken = nextPageToken ?? '';
+    try {
+      const serviceURL = `clusterList?pageSize=500&pageToken=${pageToken}`;
+
+      const formattedResponse: any = await requestAPI(serviceURL);
+      let transformClusterListData = [];
+      if (formattedResponse && formattedResponse.clusters) {
+        transformClusterListData = formattedResponse.clusters.map(
+          (data: IClusterAPIResponse) => {
+            return {
+              clusterName: data.clusterName
+            };
+          }
+        );
+      }
+      const existingClusterData = previousClustersList ?? [];
+      //setStateAction never type issue
+      const allClustersData: any = [
+        ...(existingClusterData as []),
+        ...transformClusterListData
+      ];
+
+      if (formattedResponse.nextPageToken) {
+        this.listClustersAPIService(
+          setClusterList,
+          formattedResponse.nextPageToken,
+          allClustersData
+        );
+      } else {
+        let transformClusterListData = allClustersData;
+
+        const keyLabelStructure = transformClusterListData.map(
+          (obj: { clusterName: string }) => obj.clusterName
+        );
+
+        setClusterList(keyLabelStructure);
+      }
+      if (formattedResponse?.error?.code) {
+        toast.error(formattedResponse?.error?.message, toastifyCustomStyle);
+      }
+    } catch (error) {
+      DataprocLoggingService.log('Error listing clusters', LOG_LEVEL.ERROR);
+      console.error('Error listing clusters', error);
+      toast.error('Failed to fetch clusters', toastifyCustomStyle);
+    }
+  };
+  static listSessionTemplatesAPIService = async (
+    setServerlessDataList: (value: string[]) => void,
+    setServerlessList: (value: string[]) => void,
+    nextPageToken?: string,
+    previousSessionTemplatesList?: object
+  ) => {
+    const pageToken = nextPageToken ?? '';
+    try {
+      const serviceURL = `runtimeList?pageSize=500&pageToken=${pageToken}`;
+
+      const formattedResponse: any = await requestAPI(serviceURL);
+      let transformSessionTemplateListData = [];
+      if (formattedResponse && formattedResponse.sessionTemplates) {
+        transformSessionTemplateListData =
+          formattedResponse.sessionTemplates.map((data: any) => {
+            return {
+              serverlessName: data.jupyterSession.displayName,
+              serverlessData: data
+            };
+          });
+      }
+      const existingSessionTemplateData = previousSessionTemplatesList ?? [];
+      //setStateAction never type issue
+      const allSessionTemplatesData: any = [
+        ...(existingSessionTemplateData as []),
+        ...transformSessionTemplateListData
+      ];
+
+      if (formattedResponse.nextPageToken) {
+        this.listSessionTemplatesAPIService(
+          setServerlessDataList,
+          setServerlessList,
+          formattedResponse.nextPageToken,
+          allSessionTemplatesData
+        );
+      } else {
+        let transformSessionTemplateListData = allSessionTemplatesData;
+
+        const keyLabelStructure = transformSessionTemplateListData.map(
+          (obj: { serverlessName: string }) => obj.serverlessName
+        );
+
+        setServerlessDataList(transformSessionTemplateListData);
+        setServerlessList(keyLabelStructure);
+      }
+      if (formattedResponse?.error?.code) {
+        toast.error(formattedResponse?.error?.message, toastifyCustomStyle);
+      }
+    } catch (error) {
+      DataprocLoggingService.log(
+        'Error listing session templates',
+        LOG_LEVEL.ERROR
+      );
+      console.error('Error listing session templates', error);
+      toast.error('Failed to fetch session templates', toastifyCustomStyle);
+    }
+  };
+  static listComposersAPIService = async (
+    setComposerList: (value: string[]) => void,
+    setIsLoading?: (value: boolean) => void
+  ) => {
+    try {
+      const formattedResponse: any = await requestAPI('composerList');
+      if (formattedResponse.length === 0) {
+        // Handle the case where the list is empty
+        toast.error(
+          'No composer environment in this project and region',
+          toastifyCustomStyle
+        );
+        if (setIsLoading) {
+          setIsLoading(false);
+        }
+      } else {
+        let composerEnvironmentList: string[] = [];
+        formattedResponse.forEach((data: IComposerAPIResponse) => {
+          composerEnvironmentList.push(data.name);
+        });
+        composerEnvironmentList.sort();
+        setComposerList(composerEnvironmentList);
+      }
+    } catch (error) {
+      DataprocLoggingService.log(
+        'Error listing composer environment list',
+        LOG_LEVEL.ERROR
+      );
+      console.error('Error listing composer environment list', error);
+      toast.error(
+        'Failed to fetch composer environment list',
+        toastifyCustomStyle
+      );
+    }
+  };
+  static createJobSchedulerService = async (
+    payload: IPayload,
+    app: JupyterLab,
+    setCreateCompleted: (value: boolean) => void,
+    setCreatingScheduler: (value: boolean) => void,
+    editMode: boolean
+  ) => {
+    setCreatingScheduler(true);
+    try {
+      const data: any = await requestAPI('createJobScheduler', {
+        body: JSON.stringify(payload),
+        method: 'POST'
+      });
+      if (data.error) {
+        toast.error(data.error, toastifyCustomStyle);
+        setCreatingScheduler(false);
+      } else {
+        if (editMode) {
+          toast.success(
+            `Job scheduler successfully updated`,
+            toastifyCustomStyle
+          );
+        } else {
+          toast.success(
+            `Job scheduler successfully created`,
+            toastifyCustomStyle
+          );
+        }
+        setCreatingScheduler(false);
+        setCreateCompleted(true);
+      }
+    } catch (reason) {
+      setCreatingScheduler(false);
+      console.error(`Error on POST {dataToSend}.\n${reason}`);
+    }
+  };
+
+  static editNotebookSchedulerService = async (
+    bucketName: string,
+    dagId: string,
+    setInputNotebookFilePath: (value: string) => void,
+    setEditNotebookLoading: (value: string) => void
+  ) => {
+    setEditNotebookLoading(dagId);
+    try {
+      const serviceURL = `editJobScheduler?&dag_id=${dagId}&bucket_name=${bucketName}`;
+      const formattedResponse: any = await requestAPI(serviceURL);
+      setInputNotebookFilePath(formattedResponse.input_filename);
+      setEditNotebookLoading('');
+    } catch (reason) {
+      console.error(`Error on POST {dataToSend}.\n${reason}`);
+      setEditNotebookLoading('');
+    }
+  };
+
+  static editJobSchedulerService = async (
+    bucketName: string,
+    dagId: string,
+    composerSelectedList: string,
+    setEditDagLoading: (value: string) => void,
+    setCreateCompleted?: (value: boolean) => void,
+    setJobNameSelected?: (value: string) => void,
+    setComposerSelected?: (value: string) => void,
+    setScheduleMode?: (value: scheduleMode) => void,
+    setScheduleValue?: (value: string) => void,
+
+    setInputFileSelected?: (value: string) => void,
+    setParameterDetail?: (value: string[]) => void,
+    setParameterDetailUpdated?: (value: string[]) => void,
+    setSelectedMode?: (value: string) => void,
+    setClusterSelected?: (value: string) => void,
+    setServerlessSelected?: (value: string) => void,
+    setServerlessDataSelected?: (value: {}) => void,
+    serverlessDataList?: string[],
+    setServerlessDataList?: (value: string[]) => void,
+    setServerlessList?: (value: string[]) => void,
+    setRetryCount?: (value: number) => void,
+    setRetryDelay?: (value: number) => void,
+    setEmailOnFailure?: (value: boolean) => void,
+    setEmailonRetry?: (value: boolean) => void,
+    setEmailOnSuccess?: (value: boolean) => void,
+    setEmailList?: (value: string[]) => void,
+    setStopCluster?: (value: boolean) => void,
+    setTimeZoneSelected?: (value: string) => void,
+    setEditMode?: (value: boolean) => void
+  ) => {
+    setEditDagLoading(dagId);
+    try {
+      const serviceURL = `editJobScheduler?&dag_id=${dagId}&bucket_name=${bucketName}`;
+      const formattedResponse: any = await requestAPI(serviceURL);
+      if (
+        setCreateCompleted &&
+        setJobNameSelected &&
+        setComposerSelected &&
+        setScheduleMode &&
+        setScheduleValue &&
+        setInputFileSelected &&
+        setParameterDetail &&
+        setParameterDetailUpdated &&
+        setSelectedMode &&
+        setClusterSelected &&
+        setServerlessSelected &&
+        setServerlessDataSelected &&
+        serverlessDataList &&
+        setServerlessDataList &&
+        setServerlessList &&
+        setRetryCount &&
+        setRetryDelay &&
+        setEmailOnFailure &&
+        setEmailonRetry &&
+        setEmailOnSuccess &&
+        setEmailList &&
+        setStopCluster &&
+        setTimeZoneSelected &&
+        setEditMode &&
+        dagId !== null
+      ) {
+        setJobNameSelected(dagId);
+        setComposerSelected(composerSelectedList);
+        setInputFileSelected(formattedResponse.input_filename);
+        setParameterDetail(formattedResponse.parameters);
+        setParameterDetailUpdated(formattedResponse.parameters);
+        setSelectedMode(formattedResponse.mode_selected);
+        setClusterSelected(formattedResponse.cluster_name);
+        setServerlessSelected(formattedResponse.serverless_name);
+        if (formattedResponse.mode_selected === 'serverless') {
+          await this.listSessionTemplatesAPIService(
+            setServerlessDataList,
+            setServerlessList
+          );
+          if (serverlessDataList.length > 0) {
+            const selectedData: any = serverlessDataList.filter(
+              (serverless: any) => {
+                return (
+                  serverless.serverlessName ===
+                  formattedResponse.serverless_name
+                );
+              }
+            );
+            setServerlessDataSelected(selectedData[0].serverlessData);
+          }
+        }
+        setRetryCount(formattedResponse.retry_count);
+        setRetryDelay(formattedResponse.retry_delay);
+        formattedResponse.email_failure.toLowerCase() === 'true'
+          ? setEmailOnFailure(true)
+          : setEmailOnFailure(false);
+        formattedResponse.email_delay.toLowerCase() === 'true'
+          ? setEmailonRetry(true)
+          : setEmailonRetry(false);
+        formattedResponse.email_success.toLowerCase() === 'true'
+          ? setEmailOnSuccess(true)
+          : setEmailOnSuccess(false);
+        setEmailList(formattedResponse.email);
+        formattedResponse.stop_cluster.toLowerCase() === 'true'
+          ? setStopCluster(true)
+          : setStopCluster(false);
+        if (formattedResponse.time_zone === '') {
+          setTimeZoneSelected(Intl.DateTimeFormat().resolvedOptions().timeZone);
+        } else {
+          setTimeZoneSelected(formattedResponse.time_zone);
+        }
+        setEditMode(true);
+        setCreateCompleted(false);
+        if (formattedResponse.schedule_value === '@once') {
+          setScheduleMode('runNow');
+          setScheduleValue('');
+        } else if (formattedResponse.schedule_value !== '@once') {
+          setScheduleMode('runSchedule');
+          setScheduleValue(formattedResponse.schedule_value);
+        }
+      }
+      setEditDagLoading('');
+    } catch (reason) {
+      console.error(`Error on POST {dataToSend}.\n${reason}`);
+      setEditDagLoading('');
+    }
+  };
+
+  static listDagRunsListService = async (
+    composerName: string,
+    dagId: string,
+    startDate: string,
+    endDate: string,
+    setDagRunsList: (value: IDagRunList[]) => void,
+    setDagRunId: (value: string) => void,
+    setIsLoading: (value: boolean) => void,
+
+    setBlueListDates: (value: string[]) => void,
+    setGreyListDates: (value: string[]) => void,
+    setOrangeListDates: (value: string[]) => void,
+    setRedListDates: (value: string[]) => void,
+    setGreenListDates: (value: string[]) => void,
+    setDarkGreenListDates: (value: string[]) => void
+  ) => {
+    setIsLoading(true);
+    let start_date = startDate;
+    let end_date = endDate;
+
+    try {
+      const data: any = await requestAPI(
+        `dagRun?composer=${composerName}&dag_id=${dagId}&start_date=${start_date}&end_date=${end_date}`
+      );
+      let transformDagRunListData = [];
+
+      if (data.dag_runs.length > 0) {
+        transformDagRunListData = data.dag_runs.map((dagRun: any) => {
+          return {
+            dagRunId: dagRun.dag_run_id,
+            filteredDate: new Date(dagRun.start_date)
+              .toDateString()
+              .split(' ')[2],
+            state: dagRun.state,
+            date: new Date(dagRun.start_date).toDateString(),
+            time: new Date(dagRun.start_date).toTimeString().split(' ')[0]
+          };
+        });
+
+        // Group by date first, then by status
+        const groupedDataByDateStatus = transformDagRunListData.reduce(
+          (result: any, item: any) => {
+            const date = item.filteredDate;
+            const status = item.state;
+
+            if (!result[date]) {
+              result[date] = {};
+            }
+
+            if (!result[date][status]) {
+              result[date][status] = [];
+            }
+
+            result[date][status].push(item);
+
+            return result;
+          },
+          {}
+        );
+
+        let blueList: string[] = [];
+        let greyList: string[] = [];
+        let orangeList: string[] = [];
+        let redList: string[] = [];
+        let greenList: string[] = [];
+        let darkGreenList: string[] = [];
+
+        Object.keys(groupedDataByDateStatus).forEach(dateValue => {
+          if (groupedDataByDateStatus[dateValue].running) {
+            blueList.push(dateValue);
+          } else if (groupedDataByDateStatus[dateValue].queued) {
+            greyList.push(dateValue);
+          } else if (
+            groupedDataByDateStatus[dateValue].failed &&
+            groupedDataByDateStatus[dateValue].success
+          ) {
+            orangeList.push(dateValue);
+          } else if (groupedDataByDateStatus[dateValue].failed) {
+            redList.push(dateValue);
+          } else if (
+            groupedDataByDateStatus[dateValue].success &&
+            groupedDataByDateStatus[dateValue].success.length === 1
+          ) {
+            greenList.push(dateValue);
+          } else {
+            darkGreenList.push(dateValue);
+          }
+        });
+
+        setBlueListDates(blueList);
+        setGreyListDates(greyList);
+        setOrangeListDates(orangeList);
+        setRedListDates(redList);
+        setGreenListDates(greenList);
+        setDarkGreenListDates(darkGreenList);
+
+        setDagRunsList(transformDagRunListData);
+        setDagRunId(
+          transformDagRunListData[transformDagRunListData.length - 1].dagRunId
+        );
+      } else {
+        setDagRunsList([]);
+        setDagRunId('');
+        setBlueListDates([]);
+        setGreyListDates([]);
+        setOrangeListDates([]);
+        setRedListDates([]);
+        setGreenListDates([]);
+        setDarkGreenListDates([]);
+      }
+      setIsLoading(false);
+    } catch (reason) {
+      console.error(`Error on GET credentials.\n${reason}`);
+    }
+  };
+  static listDagInfoAPIService = async (
+    setDagList: (value: IDagList[]) => void,
+    setIsLoading: (value: boolean) => void,
+    setBucketName: (value: string) => void,
+    composerSelected: string
+  ) => {
+    try {
+      const serviceURL = `dagList?composer=${composerSelected}`;
+      const formattedResponse: any = await requestAPI(serviceURL);
+      let transformDagListData = [];
+      if (formattedResponse && formattedResponse[0].dags) {
+        transformDagListData = formattedResponse[0].dags.map(
+          (dag: ISchedulerDagData) => {
+            return {
+              jobid: dag.dag_id,
+              notebookname: dag.dag_id,
+              schedule: dag.timetable_description,
+              status: dag.is_paused ? 'Paused' : 'Active',
+              scheduleInterval: dag.schedule_interval?.value
+            };
+          }
+        );
+      }
+      setDagList(transformDagListData);
+      setIsLoading(false);
+      setBucketName(formattedResponse[1]);
+    } catch (error) {
+      setIsLoading(false);
+      DataprocLoggingService.log(
+        'Error listing dag Scheduler list',
+        LOG_LEVEL.ERROR
+      );
+      console.error('Error listing dag Scheduler list', error);
+      toast.error('Failed to fetch dag Scheduler list', toastifyCustomStyle);
+    }
+  };
+  static listDagInfoAPIServiceForCreateNotebook = async (
+    setDagList: (value: IDagList[]) => void,
+    composerSelected: string
+  ) => {
+    try {
+      const serviceURL = `dagList?composer=${composerSelected}`;
+      const formattedResponse: any = await requestAPI(serviceURL);
+      let transformDagListData = [];
+      if (formattedResponse && formattedResponse[0].dags) {
+        transformDagListData = formattedResponse[0].dags.map(
+          (dag: ISchedulerDagData) => {
+            return {
+              jobid: dag.dag_id,
+              notebookname: dag.dag_id,
+              schedule: dag.timetable_description,
+              status: dag.is_paused ? 'Paused' : 'Active',
+              scheduleInterval: dag.schedule_interval?.value
+            };
+          }
+        );
+      }
+      setDagList(transformDagListData);
+    } catch (error) {
+      DataprocLoggingService.log(
+        'Error listing dag Scheduler list',
+        LOG_LEVEL.ERROR
+      );
+      console.error('Error listing dag Scheduler list', error);
+      toast.error('Failed to fetch dag Scheduler list', toastifyCustomStyle);
+    }
+  };
+  static handleDownloadSchedulerAPIService = async (
+    composerSelected: string,
+    jobid: string,
+    bucketName: string
+  ) => {
+    try {
+      const serviceURL = `dagDownload?composer=${composerSelected}&dag_id=${jobid}&bucket_name=${bucketName}`;
+      const formattedResponse: any = await requestAPI(serviceURL);
+      if (formattedResponse.status === 0) {
+        toast.success(`${jobid} downloaded successfully`, toastifyCustomStyle);
+      } else {
+        toast.error(`Failed to download the ${jobid}`, toastifyCustomStyle);
+      }
+    } catch (error) {
+      DataprocLoggingService.log('Error in Download api', LOG_LEVEL.ERROR);
+      console.error('Error in Download api', error);
+    }
+  };
+  static handleDeleteSchedulerAPIService = async (
+    composerSelected: string,
+    dag_id: string,
+    setDagList: (value: IDagList[]) => void,
+    setIsLoading: (value: boolean) => void,
+    setBucketName: (value: string) => void
+  ) => {
+    try {
+      const serviceURL = `dagDelete?composer=${composerSelected}&dag_id=${dag_id}`;
+      const deleteResponse: IUpdateSchedulerAPIResponse = await requestAPI(
+        serviceURL
+      );
+      if (deleteResponse) {
+        await SchedulerService.listDagInfoAPIService(
+          setDagList,
+          setIsLoading,
+          setBucketName,
+          composerSelected
+        );
+        toast.success(
+          `Deleted DAG with id ${dag_id}. May take a couple minutes to fully disappear`,
+          toastifyCustomStyle
+        );
+      }
+    } catch (error) {
+      DataprocLoggingService.log('Error in Delete api', LOG_LEVEL.ERROR);
+      console.error('Error in Delete api', error);
+      toast.error(`Failed to delete the ${dag_id}`, toastifyCustomStyle);
+    }
+  };
+  static handleUpdateSchedulerAPIService = async (
+    composerSelected: string,
+    dag_id: string,
+    is_status_paused: boolean,
+    setDagList: (value: IDagList[]) => void,
+    setIsLoading: (value: boolean) => void,
+    setBucketName: (value: string) => void
+  ) => {
+    try {
+      const serviceURL = `dagUpdate?composer=${composerSelected}&dag_id=${dag_id}&status=${is_status_paused}`;
+      const formattedResponse: IUpdateSchedulerAPIResponse = await requestAPI(
+        serviceURL
+      );
+      if (formattedResponse && formattedResponse.status === 0) {
+        toast.success(
+          `scheduler ${dag_id} updated successfully`,
+          toastifyCustomStyle
+        );
+        await SchedulerService.listDagInfoAPIService(
+          setDagList,
+          setIsLoading,
+          setBucketName,
+          composerSelected
+        );
+      }
+    } catch (error) {
+      DataprocLoggingService.log('Error in Update api', LOG_LEVEL.ERROR);
+      console.error('Error in Update api', error);
+      toast.error('Failed to fetch Update api', toastifyCustomStyle);
+    }
+  };
+  static listDagTaskInstancesListService = async (
+    composerName: string,
+    dagId: string,
+    dagRunId: string,
+    setDagTaskInstancesList: (value: any) => void,
+    setIsLoading: (value: boolean) => void
+  ) => {
+    setDagTaskInstancesList([]);
+    setIsLoading(true);
+    try {
+      dagRunId = encodeURIComponent(dagRunId);
+      const data: any = await requestAPI(
+        `dagRunTask?composer=${composerName}&dag_id=${dagId}&dag_run_id=${dagRunId}`
+      );
+      data.task_instances.sort(
+        (a: any, b: any) =>
+          new Date(a.start_date).getTime() - new Date(b.start_date).getTime()
+      );
+      let transformDagRunTaskInstanceListData = [];
+      transformDagRunTaskInstanceListData = data.task_instances.map(
+        (dagRunTask: any) => {
+          return {
+            tryNumber: dagRunTask.try_number,
+            taskId: dagRunTask.task_id,
+            duration: dagRunTask.duration,
+            state: dagRunTask.state,
+            date: new Date(dagRunTask.start_date).toDateString(),
+            time: new Date(dagRunTask.start_date).toTimeString().split(' ')[0]
+          };
+        }
+      );
+      setDagTaskInstancesList(transformDagRunTaskInstanceListData);
+      setIsLoading(false);
+    } catch (reason) {
+      console.error(`Error on GET credentials.\n${reason}`);
+    }
+  };
+  static listDagTaskLogsListService = async (
+    composerName: string,
+    dagId: string,
+    dagRunId: string,
+    taskId: string,
+    tryNumber: number,
+    setLogList: (value: string) => void,
+    setIsLoadingLogs: (value: boolean) => void
+  ) => {
+    try {
+      setIsLoadingLogs(true);
+      dagRunId = encodeURIComponent(dagRunId);
+      const data: any = await requestAPI(
+        `dagRunTaskLogs?composer=${composerName}&dag_id=${dagId}&dag_run_id=${dagRunId}&task_id=${taskId}&task_try_number=${tryNumber}`
+      );
+      setLogList(data.content);
+      setIsLoadingLogs(false);
+    } catch (reason) {
+      console.error(`Error on GET credentials.\n${reason}`);
+    }
+  };
+}
diff --git a/src/sessions/sessionService.tsx b/src/sessions/sessionService.tsx
index 50a5afb0..8e51c8fe 100644
--- a/src/sessions/sessionService.tsx
+++ b/src/sessions/sessionService.tsx
@@ -239,6 +239,9 @@ export class SessionService {
           setSessionsList(transformSessionListData);
           setIsLoading(false);
         }
+      } else {
+          setSessionsList([]);
+          setIsLoading(false);
       }
       if (formattedResponse?.error?.code) {
         toast.error(formattedResponse?.error?.message, toastifyCustomStyle);
diff --git a/src/utils/const.ts b/src/utils/const.ts
index 06f27c01..adfdbe54 100644
--- a/src/utils/const.ts
+++ b/src/utils/const.ts
@@ -16,7 +16,7 @@
  */
 
 import { requestAPI } from '../handler/handler';
-export const VERSION_DETAIL = '0.1.72';
+export const VERSION_DETAIL = '0.1.73';
 export const CREATE_CLUSTER_URL =
   'https://console.cloud.google.com/dataproc/clusters';
 export const CREATE_BATCH_URL =
@@ -183,4 +183,6 @@ export const NETWORK_TAG_MESSAGE =
 export const LOGIN_ERROR_MESSAGE =
   'Please navigate to Settings -> Cloud Dataproc Settings to login and continue';
 export const NOTEBOOK_TEMPLATES_LIST_URL =
-'https://api.github.com/repos/GoogleCloudPlatform/dataproc-ml-quickstart-notebooks/contents/.ci/index.json';
+  'https://api.github.com/repos/GoogleCloudPlatform/dataproc-ml-quickstart-notebooks/contents/.ci/index.json';
+export type scheduleMode = 'runNow' | 'runSchedule';
+export const scheduleValueExpression = '30 17 * * 1-5'; //Expression for schedule Value in Scheduler Jobs
diff --git a/src/utils/deletePopup.tsx b/src/utils/deletePopup.tsx
index 97387610..8a5dff1d 100644
--- a/src/utils/deletePopup.tsx
+++ b/src/utils/deletePopup.tsx
@@ -29,8 +29,15 @@ interface IDeletePopupProps {
   onDelete: () => void;
   deletePopupOpen: boolean;
   DeleteMsg: string;
+  deletingNotebook?: boolean;
 }
-function DeletePopup({ onCancel, onDelete, deletePopupOpen, DeleteMsg }: IDeletePopupProps) {
+function DeletePopup({
+  onCancel,
+  onDelete,
+  deletePopupOpen,
+  DeleteMsg,
+  deletingNotebook
+}: IDeletePopupProps) {
   return (
     <Dialog open={deletePopupOpen} onClose={onCancel}>
       <DialogTitle>Confirm deletion</DialogTitle>
@@ -39,7 +46,11 @@ function DeletePopup({ onCancel, onDelete, deletePopupOpen, DeleteMsg }: IDelete
       </DialogContent>
       <DialogActions>
         <Button onClick={onCancel}>Cancel</Button>
-        <Button onClick={onDelete}>Delete</Button>
+        {deletingNotebook ? (
+          <div className="submit-button-disable-style">DELETING</div>
+        ) : (
+          <Button onClick={onDelete}>Delete</Button>
+        )}
       </DialogActions>
     </Dialog>
   );
diff --git a/src/utils/paginationView.tsx b/src/utils/paginationView.tsx
index 752dd28b..d3a694c5 100644
--- a/src/utils/paginationView.tsx
+++ b/src/utils/paginationView.tsx
@@ -43,11 +43,23 @@ interface ICluster {
   schedulesDeletion: string;
   actions: React.ReactNode;
 }
+interface IDagList {
+  jobid: string;
+  notebookname: string;
+  schedule: string;
+  scheduleInterval: string;
+}
+
 interface IPaginationViewProps {
   pageSize: number;
   setPageSize: (value: number) => void;
   pageIndex: number;
-  allData: IBatch[] | ITemplate[]| ICluster[] | ISessionTemplateDisplay[];
+  allData:
+    | IBatch[]
+    | ITemplate[]
+    | ICluster[]
+    | ISessionTemplateDisplay[]
+    | IDagList[];
   previousPage: () => void;
   nextPage: () => void;
   canPreviousPage: boolean;
@@ -68,18 +80,18 @@ export const PaginationView = ({
     <div className="pagination-parent-view">
       <div>Rows per page: </div>
       <Select
-      className="page-size-selection"
-      value={pageSize.toString()} // Convert pageSize to string for compatibility
-      onChange={(e, { value }) => {
-        const selectedPageSize = parseInt(value as string, 10); // Parse the value to a number
-        setPageSize(selectedPageSize); // Use the parsed number as the new pageSize
-      }}
-      options={[
-        { key: '50', value: '50', text: '50' },
-        { key: '100', value: '100', text: '100' },
-        { key: '200', value: '200', text: '200' }
-      ]}
-    />
+        className="page-size-selection"
+        value={pageSize.toString()} // Convert pageSize to string for compatibility
+        onChange={(e, { value }) => {
+          const selectedPageSize = parseInt(value as string, 10); // Parse the value to a number
+          setPageSize(selectedPageSize); // Use the parsed number as the new pageSize
+        }}
+        options={[
+          { key: '50', value: '50', text: '50' },
+          { key: '100', value: '100', text: '100' },
+          { key: '200', value: '200', text: '200' }
+        ]}
+      />
       {(pageIndex + 1) * pageSize > allData.length ? (
         <div className="page-display-part">
           {pageIndex * pageSize + 1} - {allData.length} of {allData.length}
diff --git a/src/utils/tableData.tsx b/src/utils/tableData.tsx
index 7faa59b6..227d2c58 100644
--- a/src/utils/tableData.tsx
+++ b/src/utils/tableData.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
 import { ClipLoader } from 'react-spinners';
 import { Cell, Row } from 'react-table';
 
@@ -28,18 +28,29 @@ function TableData({
   page,
   prepareRow,
   tableDataCondition,
-  fromPage
+  fromPage,
+  setDagRunId,
+  selectedDagIndex
 }: any) {
-  const [selectedRowIndex, setSelectedRowIndex] = useState(-1);
+  const [selectedRowIndex, setSelectedRowIndex] = useState(selectedDagIndex);
 
   const displayData = page ? page : rows;
 
-  const handleRowClicked = (row: Row, index: number) => {
+  const handleRowClicked = (row: any, index: number) => {
     if (parseInt(row.id) === index) {
       setSelectedRowIndex(index);
+      if (fromPage === 'Dag Runs') {
+        setDagRunId(row?.original?.dagRunId);
+      }
     }
   };
 
+  useEffect(() => {
+    if (fromPage === 'Dag Runs') {
+      setSelectedRowIndex(selectedDagIndex);
+    }
+  }, [selectedDagIndex]);
+
   return (
     <table {...getTableProps()} className="clusters-list-table">
       <thead className="scroll-fix-header">
@@ -61,7 +72,11 @@ function TableData({
       </thead>
       <tbody
         {...getTableBodyProps()}
-        className={fromPage === '' ? 'gcs-table-body' : 'clusters-table-body'}
+        className={
+          fromPage === 'Dag Runs'
+            ? 'dag-runs-table-body'
+            : 'clusters-table-body'
+        }
       >
         {isLoading ? (
           <div className="spin-loader">
@@ -81,10 +96,10 @@ function TableData({
               <tr
                 {...row.getRowProps()}
                 className={
-                  fromPage === ''
+                  fromPage === 'Dag Runs'
                     ? selectedRowIndex === index
-                      ? 'gcs-row-data-parent-selected'
-                      : 'gcs-row-data-parent'
+                      ? 'dag-runs-row-data-parent-selected'
+                      : 'dag-runs-row-data-parent'
                     : 'cluster-list-data-parent'
                 }
                 onClick={() => handleRowClicked(row, index)}
diff --git a/style/base.css b/style/base.css
index 153ae185..bae5d0fb 100644
--- a/style/base.css
+++ b/style/base.css
@@ -15,7 +15,7 @@
 }
 
 /*Changing Icon for category with image add runtime*/
-.f1hgkb35 svg[data-icon='launcher:add-runtime-icon'] {
+.favyox6  svg[data-icon='launcher:add-runtime-icon'], .f1hgkb35 svg[data-icon='launcher:add-runtime-icon'] {
   content-visibility: hidden;
   display: block;
   height: 32px;
@@ -25,7 +25,7 @@
 }
 
 /*Changing Icon for category with image pyspark*/
-.f1hgkb35 svg[data-icon='launcher:pyspark-logo-icon'] {
+.favyox6  svg[data-icon='launcher:pyspark-logo-icon'], .f1hgkb35 svg[data-icon='launcher:pyspark-logo-icon'] {
   content-visibility: hidden;
   display: block;
   height: 32px;
@@ -35,7 +35,7 @@
 }
 
 /*Changing Icon for category with image cluster*/
-.f1hgkb35 svg[data-icon='launcher:clusters-icon'] {
+.favyox6  svg[data-icon='launcher:clusters-icon'], .f1hgkb35 svg[data-icon='launcher:clusters-icon'] {
   content-visibility: hidden;
   display: block;
   height: 32px;
@@ -45,7 +45,7 @@
 }
 
 /*Changing Icon for category with image python*/
-.f1hgkb35 svg[data-icon='launcher:python-logo-icon'] {
+.favyox6  svg[data-icon='launcher:python-logo-icon'], .f1hgkb35 svg[data-icon='launcher:python-logo-icon'] {
   content-visibility: hidden;
   display: block;
   height: 32px;
@@ -55,7 +55,7 @@
 }
 
 /*Changing Icon for category with image sparkR*/
-.f1hgkb35 svg[data-icon='launcher:sparkr-logo-icon'] {
+.favyox6  svg[data-icon='launcher:sparkr-logo-icon'], .f1hgkb35 svg[data-icon='launcher:sparkr-logo-icon'] {
   content-visibility: hidden;
   display: block;
   height: 32px;
@@ -65,7 +65,7 @@
 }
 
 /*Changing Icon for category with image scala*/
-.f1hgkb35 svg[data-icon='launcher:scala-logo-icon'] {
+.favyox6  svg[data-icon='launcher:scala-logo-icon'], .f1hgkb35 svg[data-icon='launcher:scala-logo-icon'] {
   content-visibility: hidden;
   display: block;
   height: 32px;
@@ -186,6 +186,12 @@ body[data-jp-theme-name='JupyterLab Dark'] .filter-section-part {
 body[data-jp-theme-name='JupyterLab Dark'] .schema-table th {
   background-color: var(--jp-layout-color2);
 }
+body[data-jp-theme-name='JupyterLab Dark'] .accordion-row-parent-header{
+  background: var(--jp-border-color3);
+}
+body[data-jp-theme-name='JupyterLab Dark'] .logo-alignment-style-accordion path{
+  fill: var(--jp-ui-font-color1);
+}
 
 body[data-jp-theme-name='JupyterLab Dark'] .job-label-style {
   background-color: var(--jp-layout-color1);
diff --git a/style/gcsBucket.css b/style/gcsBucket.css
index 160e67e8..a3a20de6 100644
--- a/style/gcsBucket.css
+++ b/style/gcsBucket.css
@@ -2,7 +2,7 @@
 
 .gcs-panel-title {
   color: var(--jp-ui-font-color0);
-  font-family: Roboto;
+  font-family: 'Roboto';
   font-size: 15px;
   font-style: normal;
   font-weight: 400;
diff --git a/style/icons/dag_task_failed_icon.svg b/style/icons/dag_task_failed_icon.svg
new file mode 100644
index 00000000..80cb6782
--- /dev/null
+++ b/style/icons/dag_task_failed_icon.svg
@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_6021_51379)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16 9C16 12.866 12.866 16 9 16C5.13401 16 2 12.866 2 9C2 5.13401 5.13401 2 9 2C12.866 2 16 5.13401 16 9ZM12.5355 6.87868L11.1213 5.46447L9 7.58579L6.87868 5.46447L5.46447 6.87868L7.58579 9L5.46447 11.1213L6.87868 12.5355L9 10.4142L11.1213 12.5355L12.5355 11.1213L10.4142 9L12.5355 6.87868Z" fill="#D50000"/>
+</g>
+<defs>
+<clipPath id="clip0_6021_51379">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/style/icons/dag_task_success_icon.svg b/style/icons/dag_task_success_icon.svg
new file mode 100644
index 00000000..da86a071
--- /dev/null
+++ b/style/icons/dag_task_success_icon.svg
@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_6021_51380)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 1.5C4.86 1.5 1.5 4.86 1.5 9C1.5 13.14 4.86 16.5 9 16.5C13.14 16.5 16.5 13.14 16.5 9C16.5 4.86 13.14 1.5 9 1.5ZM7.5 12.75L3.75 9L4.8075 7.9425L7.5 10.6275L13.1925 4.935L14.25 6L7.5 12.75Z" fill="#188038"/>
+</g>
+<defs>
+<clipPath id="clip0_6021_51380">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/style/icons/download_icon.svg b/style/icons/download_icon.svg
new file mode 100644
index 00000000..e0ad5c9b
--- /dev/null
+++ b/style/icons/download_icon.svg
@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_5994_80754)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.75 2.25H11.25V6.75H14.25L9 12L3.75 6.75H6.75V2.25ZM3.75 13.5H14.25V15H3.75V13.5Z" fill="#616161"/>
+</g>
+<defs>
+<clipPath id="clip0_5994_80754">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/style/icons/edit_notebook_icon.svg b/style/icons/edit_notebook_icon.svg
new file mode 100644
index 00000000..9731f35f
--- /dev/null
+++ b/style/icons/edit_notebook_icon.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 20 20" height="18px" viewBox="0 0 20 20" width="18px" fill="#000000"><rect fill="none" height="20" width="20"/><path d="M3,5h9v1.5H3V5z M3,11.25h6v1.5H3V11.25z M3,8.12h9v1.5H3V8.12z M16.78,11.99l0.65-0.65c0.29-0.29,0.29-0.77,0-1.06 l-0.71-0.71c-0.29-0.29-0.77-0.29-1.06,0l-0.65,0.65L16.78,11.99z M16.19,12.58L11.77,17H10v-1.77l4.42-4.42L16.19,12.58z"/></svg>
\ No newline at end of file
diff --git a/style/icons/keyboard_arrow_down.svg b/style/icons/keyboard_arrow_down.svg
new file mode 100644
index 00000000..1452483e
--- /dev/null
+++ b/style/icons/keyboard_arrow_down.svg
@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_5710_31872)">
+<path d="M9 12.4L3 6.4L4.4 5L9 9.6L13.6 5L15 6.4L9 12.4Z" fill="#444746"/>
+</g>
+<defs>
+<clipPath id="clip0_5710_31872">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/style/icons/notebook_scheduler.svg b/style/icons/notebook_scheduler.svg
new file mode 100644
index 00000000..8317a13c
--- /dev/null
+++ b/style/icons/notebook_scheduler.svg
@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_5710_31114)">
+<path d="M4.51875 7.74375L5.98125 8.23125C6.16875 7.88125 6.3625 7.54375 6.5625 7.21875C6.7625 6.88125 6.975 6.55 7.2 6.225L6.16875 6.09375L4.51875 7.74375ZM7.10625 8.9625L9.01875 10.875C9.61875 10.5625 10.1875 10.225 10.725 9.8625C11.2625 9.4875 11.7688 9.0625 12.2438 8.5875C12.9938 7.8375 13.6125 7.00625 14.1 6.09375C14.6 5.18125 14.85 4.2 14.85 3.15C13.7875 3.15 12.8 3.39375 11.8875 3.88125C10.975 4.36875 10.1438 4.9875 9.39375 5.7375C8.91875 6.2125 8.49375 6.71875 8.11875 7.25625C7.74375 7.79375 7.40625 8.3625 7.10625 8.9625ZM10.2938 7.70625C10.0313 7.44375 9.9 7.125 9.9 6.75C9.9 6.375 10.0313 6.05625 10.2938 5.79375C10.5563 5.53125 10.875 5.4 11.25 5.4C11.625 5.4 11.9438 5.53125 12.2063 5.79375C12.4688 6.05625 12.6 6.375 12.6 6.75C12.6 7.125 12.4688 7.44375 12.2063 7.70625C11.9438 7.96875 11.625 8.1 11.25 8.1C10.875 8.1 10.5563 7.96875 10.2938 7.70625ZM10.2563 13.4812L11.9063 11.8312L11.775 10.8C11.45 11.025 11.1188 11.2375 10.7813 11.4375C10.4563 11.6375 10.1188 11.8312 9.76875 12.0187L10.2563 13.4812ZM16.0125 1.96875C16.3375 3.24375 16.2188 4.55625 15.6563 5.90625C15.1063 7.25625 14.325 8.4375 13.3125 9.45C13.3125 9.45 13.2625 9.50625 13.1625 9.61875C13.0625 9.71875 13.0125 9.76875 13.0125 9.76875L13.2375 11.6625C13.2625 11.875 13.2375 12.0812 13.1625 12.2812C13.1 12.4687 12.9938 12.6375 12.8438 12.7875L9.65625 15.975L8.38125 12.1687L5.85 9.61875L2.025 8.34375L5.2125 5.1375C5.3625 4.9875 5.53125 4.88125 5.71875 4.81875C5.91875 4.74375 6.125 4.71875 6.3375 4.74375L8.23125 4.9875C8.23125 4.9875 8.2875 4.93125 8.4 4.81875C8.525 4.70625 8.5875 4.65 8.5875 4.65C9.6 3.6375 10.7688 2.85625 12.0938 2.30625C13.4313 1.74375 14.7375 1.63125 16.0125 1.96875ZM3.3375 11.7937C3.7375 11.4187 4.20625 11.2375 4.74375 11.25C5.29375 11.25 5.76875 11.45 6.16875 11.85C6.54375 12.225 6.73125 12.6812 6.73125 13.2188C6.74375 13.7437 6.575 14.2062 6.225 14.6062C5.6875 15.2187 5.0125 15.6062 4.2 15.7687C3.4 15.9187 2.6 16.0625 1.8 16.2C1.9375 15.4125 2.08125 14.625 2.23125 13.8375C2.38125 13.0375 2.75 12.3562 3.3375 11.7937ZM4.18125 12.8625C3.96875 13.075 3.825 13.325 3.75 13.6125C3.6875 13.9 3.6125 14.1875 3.525 14.475C3.8125 14.4 4.09375 14.325 4.36875 14.25C4.65625 14.175 4.9 14.0312 5.1 13.8187C5.2375 13.6687 5.31875 13.4937 5.34375 13.2937C5.38125 13.0937 5.33125 12.925 5.19375 12.7875C5.05625 12.65 4.8875 12.5937 4.6875 12.6187C4.5 12.6437 4.33125 12.725 4.18125 12.8625Z" fill="#616161"/>
+</g>
+<defs>
+<clipPath id="clip0_5710_31114">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/style/icons/pause_icon.svg b/style/icons/pause_icon.svg
new file mode 100644
index 00000000..9c64f9e0
--- /dev/null
+++ b/style/icons/pause_icon.svg
@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_5994_80731)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7 3H3V15H7V3ZM15 3H11V15H15V3Z" fill="#616161"/>
+</g>
+<defs>
+<clipPath id="clip0_5994_80731">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/style/icons/play_icon.svg b/style/icons/play_icon.svg
new file mode 100644
index 00000000..7717e4ac
--- /dev/null
+++ b/style/icons/play_icon.svg
@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_5994_80719)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 1.5C4.86 1.5 1.5 4.86 1.5 9C1.5 13.14 4.86 16.5 9 16.5C13.14 16.5 16.5 13.14 16.5 9C16.5 4.86 13.14 1.5 9 1.5ZM7.5 12.375V5.625L12 9L7.5 12.375Z" fill="#616161"/>
+</g>
+<defs>
+<clipPath id="clip0_5994_80719">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/style/icons/scheduled_notebooks_icon.svg b/style/icons/scheduled_notebooks_icon.svg
new file mode 100644
index 00000000..07fe5e23
--- /dev/null
+++ b/style/icons/scheduled_notebooks_icon.svg
@@ -0,0 +1,10 @@
+<svg width="52" height="52" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_5994_27021)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M21.7493 13.25H37.3327V17.5H21.7493V13.25ZM21.7493 20.3333H37.3327V24.5833H21.7493V20.3333ZM21.7493 27.4167H37.3327V31.6667H21.7493V27.4167ZM21.7493 34.5H37.3327V38.75H21.7493V34.5ZM14.666 13.25H18.916V17.5H14.666V13.25ZM14.666 20.3333H18.916V24.5833H14.666V20.3333ZM14.666 27.4167H18.916V31.6667H14.666V27.4167ZM14.666 34.5H18.916V38.75H14.666V34.5Z" fill="#3367D6"/>
+</g>
+<defs>
+<clipPath id="clip0_5994_27021">
+<rect width="34" height="34" fill="white" transform="translate(9 9)"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/style/index.css b/style/index.css
index 084af14d..d25af759 100644
--- a/style/index.css
+++ b/style/index.css
@@ -14,4 +14,5 @@
 @import url('paginationView.css');
 @import url('runtimeTemplate.css');
 @import url('gcsBucket.css');
+@import url('notebookScheduler.css');
 @import url('notebookTemplates.css');
diff --git a/style/index.js b/style/index.js
index 5318ad84..dac54576 100644
--- a/style/index.js
+++ b/style/index.js
@@ -31,4 +31,5 @@ import './databaseInfo.css';
 import './paginationView.css';
 import './runtimeTemplate.css';
 import './gcsBucket.css';
+import './notebookScheduler.css';
 import './notebookTemplates.css';
diff --git a/style/job.css b/style/job.css
index 440f8224..b1b2288d 100644
--- a/style/job.css
+++ b/style/job.css
@@ -202,7 +202,7 @@
   font-weight: 500;
   font-size: 13px;
   line-height: 32px;
-  font-family: Roboto;
+  font-family: 'Roboto';
   /* identical to box height, or 246% */
 
   display: flex;
diff --git a/style/notebookScheduler.css b/style/notebookScheduler.css
new file mode 100644
index 00000000..7f08e5c8
--- /dev/null
+++ b/style/notebookScheduler.css
@@ -0,0 +1,238 @@
+@import url(https://fonts.googleapis.com/css?family=Roboto);
+
+.select-text-overlay-scheduler {
+  width: 512px;
+  margin: 20px;
+}
+
+.execution-history-parent {
+  margin: 20px;
+}
+
+.input-style-scheduler {
+  width: 100%;
+  background: var(--jp-layout-color0);
+  min-height: 40px;
+  outline: none;
+}
+
+.select-job-style-scheduler {
+  background: var(--jp-layout-color0);
+  border-radius: 4px;
+  width: 100%;
+  min-height: 36px;
+  outline: none;
+}
+
+.create-scheduler-label-style {
+  font-size: 14px;
+}
+
+.create-scheduler-form-element {
+  margin-top: 15px;
+}
+
+.create-scheduler-form-element-input-file {
+  margin-top: 15px;
+  display: flex;
+  align-items: center;
+}
+
+.edit-notebook-style {
+  cursor: pointer;
+  margin-top: 10px;
+  padding-left: 15px;
+}
+
+.create-job-scheduler-title {
+  padding: 0px 15px;
+  height: 48px;
+  font-style: normal;
+  font-weight: 400;
+  font-size: 18px;
+  line-height: 24px;
+  display: flex;
+  align-items: center;
+  color: var(--jp-ui-font-color0);
+}
+
+.create-scheduler-label {
+  margin-top: 15px;
+  font-style: normal;
+  font-weight: 500;
+  font-size: 20px;
+  line-height: 28px;
+  display: flex;
+  align-items: center;
+  color: var(--jp-ui-font-color0);
+}
+
+.create-scheduler-style {
+  width: 512px;
+  height: 36px;
+  background: var(--jp-layout-color0);
+  margin-top: 10px;
+  outline: none;
+}
+
+.execution-history-left-wrapper {
+  width: 320px;
+  border: 1px solid var(--jp-layout-color3);
+}
+.execution-history-main-wrapper {
+  display: flex;
+}
+
+.execution-history-right-wrapper {
+  width: calc(100% - 320px);
+  border: 1px solid var(--jp-layout-color3);
+  border-left: none;
+}
+
+.dag-runs-list-table-parent {
+  height: 400px;
+  overflow: auto;
+}
+
+.scheduler-back-arrow-icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  padding-left: 10px;
+}
+
+.dag-runs-table-body {
+  overflow-y: auto;
+  max-height: 55vh;
+}
+
+.dag-runs-row-data-parent:hover {
+  background: var(--jp-layout-color2);
+}
+
+.dag-runs-row-data-parent-selected {
+  height: 29px;
+  background: var(--jp-brand-color4);
+  color: var(--jp-brand-color1);
+}
+
+.dag-runs-row-data-parent {
+  background: var(--jp-layout-color0);
+  height: 29px;
+  font-style: normal;
+  font-weight: 400;
+  font-size: 13px;
+  line-height: 18px;
+  color: var(--jp-ui-font-color1);
+  cursor: pointer;
+}
+
+.execution-history-title {
+  display: flex;
+}
+
+.accordion-row-parent {
+  display: flex;
+  width: 100%;
+  height: 30px;
+  justify-content: center;
+  align-items: center;
+  border-bottom: 1px solid var(--jp-layout-color3);
+}
+
+.accordion-row-parent-header {
+  background: rgba(0, 0, 0, 0.04);
+  font-style: normal;
+  font-weight: 500;
+  font-size: 13px;
+  height: 29px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  line-height: 18px;
+  color: var(--jp-ui-font-color0);
+  border-bottom: 1px solid var(--jp-layout-color3);
+}
+
+.accordion-row-data {
+  padding-left: 15px;
+  width: 33%;
+}
+
+.logo-row-container {
+  display: flex;
+}
+
+.logo-alignment-style-accordion {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  cursor: pointer;
+}
+
+.logs-content-style {
+  overflow: auto;
+  max-height: 400px;
+  margin: 10px;
+  border-bottom: 1px solid var(--jp-layout-color3);
+}
+
+.accordion-row-data-expand-logo {
+  width: 30px;
+}
+
+.dag-runs-table-data-state-success {
+  background-color: #34a853;
+  color: var(--jp-border-color3);
+  border-radius: 22px;
+  margin: 5px 15px;
+  display: flex;
+  width: 71px;
+  text-transform: capitalize;
+  justify-content: center;
+  align-items: center;
+}
+.dag-runs-table-data-state-failure {
+  background-color: #ea3323;
+  color: var(--jp-border-color3);
+  border-radius: 22px;
+  margin: 5px 15px;
+  display: flex;
+  width: 71px;
+  text-transform: capitalize;
+  justify-content: center;
+  align-items: center;
+}
+.dag-runs-table-data-state-queued {
+  background-color: #808080;
+  color: var(--jp-border-color3);
+  border-radius: 22px;
+  margin: 5px 15px;
+  display: flex;
+  width: 71px;
+  text-transform: capitalize;
+  justify-content: center;
+  align-items: center;
+}
+.dag-runs-table-data-state-running {
+  background-color: #1a73e8;
+  color: var(--jp-border-color3);
+  border-radius: 22px;
+  margin: 5px 15px;
+  display: flex;
+  width: 71px;
+  text-transform: capitalize;
+  justify-content: center;
+  align-items: center;
+}
+
+.dag-run-state-parent {
+  border-bottom: 1px solid var(--jp-layout-color3);
+  height: 24px;
+}
+
+.execution-history-header {
+  height: 48px;
+  display: flex;
+}
diff --git a/style/submitJob.css b/style/submitJob.css
index 8dd84a68..65d47732 100644
--- a/style/submitJob.css
+++ b/style/submitJob.css
@@ -29,7 +29,7 @@
   background: var(--jp-layout-color0);
   color: var(--jp-ui-font-color2);
   border: none;
-  font-family: ROBOTO;
+  font-family: 'Roboto';
   border-radius: 4px;
 }
 .submit-job-message-input {
diff --git a/third-party-licenses.txt b/third-party-licenses.txt
index b2a76d7d..654d7514 100644
--- a/third-party-licenses.txt
+++ b/third-party-licenses.txt
@@ -1,15 +1,237 @@
 This file was generated with the generate-license-file npm package!
 https://www.npmjs.com/package/generate-license-file
 
+The following npm packages may be included in this product:
+
+ - @ant-design/colors@7.0.2
+ - @ant-design/icons@5.3.0
+
+These packages each contain the following license and notice below:
+
+MIT LICENSE
+
+Copyright (c) 2018-present Ant UED, https://xtech.antfin.com/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm packages may be included in this product:
+
+ - @ant-design/cssinjs@1.18.4
+ - rc-input@1.4.3
+ - rc-motion@2.9.0
+ - rc-overflow@1.3.2
+ - rc-picker@4.1.4
+ - rc-resize-observer@1.4.0
+ - rc-segmented@2.3.0
+ - rc-textarea@1.6.3
+
+These packages each contain the following license and notice below:
+
+The MIT License (MIT)
+
+Copyright (c) 2019-present afc163
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm package may be included in this product:
+
+ - @ant-design/icons-svg@4.4.2
+
+This package contains the following license and notice below:
+
+<h1 align="center">
+Ant Design Icons
+</h1>
+
+<p align="center">
+⭐ The abstract node of the Ant Design SVG icons.
+</p>
+
+<div align="center">
+
+[![NPM version](https://img.shields.io/npm/v/@ant-design/icons-svg.svg?style=flat)](https://npmjs.org/package/@ant-design/icons-svg)
+[![NPM downloads](http://img.shields.io/npm/dm/@ant-design/icons-svg.svg?style=flat)](https://npmjs.org/package/@ant-design/icons-svg)
+
+</div>
+
+Check [all icons list](https://github.com/ant-design/ant-design-icons/issues/227).
+
+## Install
+
+```bash
+# use yarn
+$ yarn add @ant-design/icons-svg
+
+# or use npm
+$ npm install @ant-design/icons-svg --save
+```
+
+## Use Library Adapter
+
+- React: See [@ant-design/icons](../icons-react) to learn about detail usage.
+
+## Contribution Guide 贡献指南
+
+See contribution guide. [English](./docs/ContributionGuide.md) ｜ [中文](./docs/ContributionGuide.zh-CN.md)
+
+## Get started
+
+```ts
+import { AccountBookOutlined } from '@ant-design/icons-svg';
+// or
+// import AccountBookOutlined from '@ant-design/icons-svg/es/asn/AccountBookOutlined';
+
+console.log(AccountBookOutlined);
+// ==>
+// {
+//   name: 'account-book',
+//   theme: 'outlined',
+//   icon: {
+//     tag: 'svg',
+//     attrs: {
+//       viewBox: '64 64 896 896',
+//       focusable: 'false'
+//     },
+//     children: [
+//       {
+//         tag: 'path',
+//         attrs: {
+//           d:
+//             'M880 184H712v-64c0-4.4-3.6-8-8-8h- ...'
+//         }
+//       }
+//     ]
+//   }
+// };
+```
+
+- Interfaces
+
+This library export all SVG files as `IconDefinition`.
+
+```ts
+// types.d.ts
+export declare type ThemeType = 'filled' | 'outlined' | 'twotone';
+
+export interface AbstractNode {
+  tag: string;
+  attrs: {
+    [key: string]: string;
+  };
+  children?: AbstractNode[];
+}
+
+export interface IconDefinition {
+  name: string; // kebab-case-style
+  theme: ThemeType;
+  icon:
+    | ((primaryColor: string, secondaryColor: string) => AbstractNode)
+    | AbstractNode;
+}
+```
+
+## Render Helpers
+
+```ts
+import { AccountBookFilled } from '@ant-design/icons-svg';
+import { renderIconDefinitionToSVGElement } from '@ant-design/icons-svg/es/helpers';
+
+const svgHTMLString = renderIconDefinitionToSVGElement(AccountBookFilled, {
+  extraSVGAttrs: { width: '1em', height: '1em', fill: 'currentColor' }
+});
+
+console.log(svgHTMLString);
+// ==>
+// '<svg viewBox="64 64 896 896" width="1em" height="1em" fill="currentColor"><path d="M880 184H712v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H384v-64c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v64H144c-17.7 0-32 14.3-32 32v664c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V216c0-17.7-14.3-32-32-32zM648.3 426.8l-87.7 161.1h45.7c5.5 0 10 4.5 10 10v21.3c0 5.5-4.5 10-10 10h-63.4v29.7h63.4c5.5 0 10 4.5 10 10v21.3c0 5.5-4.5 10-10 10h-63.4V752c0 5.5-4.5 10-10 10h-41.3c-5.5 0-10-4.5-10-10v-51.8h-63.1c-5.5 0-10-4.5-10-10v-21.3c0-5.5 4.5-10 10-10h63.1v-29.7h-63.1c-5.5 0-10-4.5-10-10v-21.3c0-5.5 4.5-10 10-10h45.2l-88-161.1c-2.6-4.8-.9-10.9 4-13.6 1.5-.8 3.1-1.2 4.8-1.2h46c3.8 0 7.2 2.1 8.9 5.5l72.9 144.3 73.2-144.3a10 10 0 0 1 8.9-5.5h45c5.5 0 10 4.5 10 10 .1 1.7-.3 3.3-1.1 4.8z" /></svg>'
+```
+
+- Interfaces
+
+```ts
+declare function renderIconDefinitionToSVGElement(
+  icon: IconDefinition,
+  options?: HelperRenderOptions
+): string;
+
+interface HelperRenderOptions {
+  placeholders?: {
+    primaryColor?: string; // default #333
+    secondaryColor?: string; // default #E6E6E6
+  };
+  extraSVGAttrs?: {
+    [key: string]: string;
+  };
+}
+```
+
+-----------
+
+The following npm packages may be included in this product:
+
+ - @ant-design/react-slick@1.0.2
+ - json2mq@0.2.0
+
+These packages each contain the following license and notice below:
+
+The MIT License (MIT)
+
+Copyright (c) 2014 Kiran Abburi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+-----------
+
 The following npm packages may be included in this product:
 
  - @babel/code-frame@7.22.13
- - @babel/helper-module-imports@7.22.5
+ - @babel/helper-module-imports@7.22.15
  - @babel/helper-string-parser@7.22.5
- - @babel/helper-validator-identifier@7.22.5
- - @babel/highlight@7.22.13
- - @babel/runtime@7.22.11
- - @babel/types@7.22.11
+ - @babel/helper-validator-identifier@7.22.20
+ - @babel/highlight@7.22.20
+ - @babel/runtime@7.23.6
+ - @babel/runtime@7.23.9
+ - @babel/types@7.23.0
 
 These packages each contain the following license and notice below:
 
@@ -40,7 +262,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 The following npm package may be included in this product:
 
- - @codemirror/state@6.2.1
+ - @codemirror/state@6.3.1
 
 This package contains the following license and notice below:
 
@@ -68,10 +290,27 @@ THE SOFTWARE.
 
 -----------
 
+The following npm package may be included in this product:
+
+ - @ctrl/tinycolor@3.6.1
+
+This package contains the following license and notice below:
+
+Copyright (c) Scott Cooper <scttcper@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
 The following npm packages may be included in this product:
 
  - @emotion/babel-plugin@11.11.0
  - @emotion/cache@11.11.0
+ - @emotion/hash@0.8.0
  - @emotion/hash@0.9.1
  - @emotion/is-prop-valid@1.2.1
  - @emotion/memoize@0.8.1
@@ -79,6 +318,7 @@ The following npm packages may be included in this product:
  - @emotion/serialize@1.1.2
  - @emotion/sheet@1.2.2
  - @emotion/styled@11.11.0
+ - @emotion/unitless@0.7.5
  - @emotion/unitless@0.8.1
  - @emotion/use-insertion-effect-with-fallbacks@1.0.1
  - @emotion/utils@1.2.1
@@ -112,10 +352,14 @@ SOFTWARE.
 
 The following npm packages may be included in this product:
 
- - @floating-ui/core@1.4.1
- - @floating-ui/dom@1.5.1
- - @floating-ui/react-dom@2.0.2
- - @floating-ui/utils@0.1.1
+ - @floating-ui/core@1.5.0
+ - @floating-ui/core@1.6.0
+ - @floating-ui/dom@1.5.3
+ - @floating-ui/dom@1.6.3
+ - @floating-ui/react-dom@2.0.4
+ - @floating-ui/react-dom@2.0.8
+ - @floating-ui/utils@0.1.6
+ - @floating-ui/utils@0.2.1
 
 These packages each contain the following license and notice below:
 
@@ -187,7 +431,7 @@ to represent the company, product, or service to which they refer.**
 
 The following npm package may be included in this product:
 
- - @jupyter/ydoc@1.0.2
+ - @jupyter/ydoc@1.1.1
 
 This package contains the following license and notice below:
 
@@ -204,7 +448,7 @@ The API documentation is available [there](https://jupyter-ydoc.readthedocs.io/e
 
 The following npm package may be included in this product:
 
- - @jupyterlab/application@4.0.5
+ - @jupyterlab/application@4.0.7
 
 This package contains the following license and notice below:
 
@@ -217,7 +461,7 @@ with which JupyterLab plugins may be registered.
 
 The following npm package may be included in this product:
 
- - @jupyterlab/apputils@4.1.5
+ - @jupyterlab/apputils@4.1.7
 
 This package contains the following license and notice below:
 
@@ -229,7 +473,7 @@ A JupyterLab package which provides a collection of utilities and UI elements fo
 
 The following npm package may be included in this product:
 
- - @jupyterlab/codeeditor@4.0.5
+ - @jupyterlab/codeeditor@4.0.7
 
 This package contains the following license and notice below:
 
@@ -243,7 +487,7 @@ and the [file editor](../fileeditor).
 
 The following npm package may be included in this product:
 
- - @jupyterlab/coreutils@6.0.5
+ - @jupyterlab/coreutils@6.0.7
 
 This package contains the following license and notice below:
 
@@ -259,7 +503,7 @@ This package is intended for use within both Node.js and browser environments.
 
 The following npm package may be included in this product:
 
- - @jupyterlab/docregistry@4.0.5
+ - @jupyterlab/docregistry@4.0.7
 
 This package contains the following license and notice below:
 
@@ -277,7 +521,7 @@ The document registry is a singleton on the [application](../application).
 
 The following npm package may be included in this product:
 
- - @jupyterlab/launcher@4.0.5
+ - @jupyterlab/launcher@4.0.7
 
 This package contains the following license and notice below:
 
@@ -292,7 +536,7 @@ JupyterLab extensions may register themselves with the launcher in order to show
 
 The following npm package may be included in this product:
 
- - @jupyterlab/mainmenu@4.0.5
+ - @jupyterlab/mainmenu@4.0.7
 
 This package contains the following license and notice below:
 
@@ -304,10 +548,10 @@ A JupyterLab extension which provides the application menubar.
 
 The following npm packages may be included in this product:
 
- - @jupyterlab/nbformat@4.0.5
- - @jupyterlab/settingregistry@4.0.5
- - @jupyterlab/statedb@4.0.5
- - @jupyterlab/statusbar@4.0.5
+ - @jupyterlab/nbformat@4.0.7
+ - @jupyterlab/settingregistry@4.0.7
+ - @jupyterlab/statedb@4.0.7
+ - @jupyterlab/statusbar@4.0.7
  - @lumino/algorithm@2.0.1
  - @lumino/application@2.2.1
  - @lumino/collections@2.0.1
@@ -331,7 +575,7 @@ These packages each contain the following license and notice below:
 
 The following npm package may be included in this product:
 
- - @jupyterlab/observables@5.0.5
+ - @jupyterlab/observables@5.0.7
 
 This package contains the following license and notice below:
 
@@ -343,7 +587,7 @@ A JupyterLab package which provides data structures (such as strings, lists, and
 
 The following npm package may be included in this product:
 
- - @jupyterlab/rendermime-interfaces@3.8.5
+ - @jupyterlab/rendermime-interfaces@3.8.7
 
 This package contains the following license and notice below:
 
@@ -366,7 +610,7 @@ Examples can be found in [@jupyterlab/vega5-extension](../vega5-extension) and [
 
 The following npm package may be included in this product:
 
- - @jupyterlab/rendermime@4.0.5
+ - @jupyterlab/rendermime@4.0.7
 
 This package contains the following license and notice below:
 
@@ -385,7 +629,7 @@ The rendermime is a singleton on the [application](../application).
 
 The following npm package may be included in this product:
 
- - @jupyterlab/services@7.0.5
+ - @jupyterlab/services@7.0.7
 
 This package contains the following license and notice below:
 
@@ -623,7 +867,7 @@ _The diagram can be edited on [diagrams.net](https://diagrams.net) by importing
 
 The following npm package may be included in this product:
 
- - @jupyterlab/translation@4.0.5
+ - @jupyterlab/translation@4.0.7
 
 This package contains the following license and notice below:
 
@@ -635,7 +879,7 @@ Translation utilities.
 
 The following npm package may be included in this product:
 
- - @jupyterlab/ui-components@4.0.5
+ - @jupyterlab/ui-components@4.0.7
 
 This package contains the following license and notice below:
 
@@ -1108,15 +1352,18 @@ In order to use `for-await...of` loops in TypeScript, you will need to use `ES20
 
 The following npm packages may be included in this product:
 
- - @mui/base@5.0.0-beta.13
- - @mui/core-downloads-tracker@5.14.7
- - @mui/icons-material@5.14.7
- - @mui/material@5.14.7
- - @mui/private-theming@5.14.7
- - @mui/styled-engine@5.14.7
- - @mui/system@5.14.7
- - @mui/types@7.2.4
- - @mui/utils@5.14.7
+ - @mui/base@5.0.0-beta.20
+ - @mui/base@5.0.0-beta.37
+ - @mui/core-downloads-tracker@5.14.14
+ - @mui/icons-material@5.14.14
+ - @mui/material@5.14.14
+ - @mui/private-theming@5.14.14
+ - @mui/styled-engine@5.14.14
+ - @mui/system@5.14.14
+ - @mui/types@7.2.12
+ - @mui/types@7.2.13
+ - @mui/utils@5.15.11
+ - @mui/utils@5.15.3
 
 These packages each contain the following license and notice below:
 
@@ -1144,6 +1391,36 @@ SOFTWARE.
 
 -----------
 
+The following npm package may be included in this product:
+
+ - @mui/x-date-pickers@6.19.4
+
+This package contains the following license and notice below:
+
+MIT License
+
+Copyright (c) 2020 Material-UI SAS
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+-----------
+
 The following npm package may be included in this product:
 
  - @popperjs/core@2.11.8
@@ -1173,6 +1450,120 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 -----------
 
+The following npm packages may be included in this product:
+
+ - @rc-component/color-picker@1.5.2
+ - rc-mentions@2.10.1
+
+These packages each contain the following license and notice below:
+
+The MIT License (MIT)
+
+Copyright (c) 2019-present alipay.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm packages may be included in this product:
+
+ - @rc-component/context@1.4.0
+ - async-validator@4.2.5
+ - rc-collapse@3.7.2
+ - rc-dialog@9.3.4
+ - rc-input-number@9.0.0
+ - rc-menu@9.12.4
+ - rc-notification@5.3.0
+ - rc-pagination@4.0.4
+ - rc-progress@3.5.1
+ - rc-rate@2.12.0
+ - rc-steps@6.0.1
+ - rc-switch@4.1.0
+ - rc-tabs@14.0.0
+
+These packages each contain the following license and notice below:
+
+The MIT License (MIT)
+
+Copyright (c) 2014-present yiminghe
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm packages may be included in this product:
+
+ - @rc-component/mini-decimal@1.1.0
+ - @rc-component/mutate-observer@1.1.0
+ - @rc-component/portal@1.1.2
+ - @rc-component/tour@1.12.3
+ - rc-field-form@1.41.0
+
+These packages each contain the following license and notice below:
+
+MIT License
+
+Copyright (c) 2019-present react-component
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+-----------
+
+The following npm packages may be included in this product:
+
+ - @rc-component/trigger@1.18.3
+ - rc-dropdown@4.1.0
+ - rc-slider@10.5.0
+
+These packages each contain the following license and notice below:
+
+The MIT License (MIT)
+Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
 The following npm package may be included in this product:
 
  - @react-dnd/asap@4.0.1
@@ -1264,8 +1655,8 @@ SOFTWARE.
 
 The following npm packages may be included in this product:
 
- - @rjsf/core@5.12.1
- - @rjsf/utils@5.12.1
+ - @rjsf/core@5.13.2
+ - @rjsf/utils@5.13.2
 
 These packages each contain the following license and notice below:
 
@@ -1475,44 +1866,15 @@ limitations under the License.
 
 The following npm packages may be included in this product:
 
- - @types/hoist-non-react-statics@3.3.1
- - @types/parse-json@4.0.0
-
-These packages each contain the following license and notice below:
-
-MIT License
-
-    Copyright (c) Microsoft Corporation. All rights reserved.
-
-    Permission is hereby granted, free of charge, to any person obtaining a copy
-    of this software and associated documentation files (the "Software"), to deal
-    in the Software without restriction, including without limitation the rights
-    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-    copies of the Software, and to permit persons to whom the Software is
-    furnished to do so, subject to the following conditions:
-
-    The above copyright notice and this permission notice shall be included in all
-    copies or substantial portions of the Software.
-
-    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-    SOFTWARE
-
------------
-
-The following npm packages may be included in this product:
-
- - @types/node@20.5.7
- - @types/prop-types@15.7.5
- - @types/react-is@18.2.1
- - @types/react-transition-group@4.4.6
- - @types/react@18.2.21
- - @types/scheduler@0.16.3
- - @types/uuid@9.0.3
+ - @types/hoist-non-react-statics@3.3.4
+ - @types/node@20.8.7
+ - @types/parse-json@4.0.1
+ - @types/prop-types@15.7.11
+ - @types/react-transition-group@4.4.10
+ - @types/react-transition-group@4.4.8
+ - @types/react@18.2.30
+ - @types/scheduler@0.16.5
+ - @types/uuid@9.0.6
 
 These packages each contain the following license and notice below:
 
@@ -1595,6 +1957,81 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
 
 -----------
 
+The following npm package may be included in this product:
+
+ - antd@5.14.1
+
+This package contains the following license and notice below:
+
+MIT LICENSE
+
+Copyright (c) 2015-present Ant UED, https://xtech.antfin.com/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm package may be included in this product:
+
+ - array-tree-filter@2.1.0
+
+This package contains the following license and notice below:
+
+# array-tree-filter
+
+Filter by keys in array tree.
+
+[![NPM version](https://img.shields.io/npm/v/array-tree-filter.svg?style=flat)](https://npmjs.org/package/array-tree-filter)
+[![Build Status](https://img.shields.io/travis/afc163/array-tree-filter.svg?style=flat)](https://travis-ci.org/afc163/array-tree-filter)
+[![David Status](https://img.shields.io/david/afc163/array-tree-filter.svg?style=flat)](https://david-dm.org/afc163/array-tree-filter)
+[![NPM downloads](http://img.shields.io/npm/dm/array-tree-filter.svg?style=flat)](https://npmjs.org/package/array-tree-filter)
+
+```js
+import arrayTreeFilter from 'array-tree-filter';
+
+const data = [{
+  value: 'a',
+  children: [{
+    value: 'b',
+    children: [{
+      value: 'c'
+    }, {
+      value: 'd',
+    }]
+  }],
+}];
+const values = ['a', 'b', 'c'];
+const result = arrayTreeFilter(
+  data, (item, level) => item.value === values[level]
+);
+
+console.log(result);
+// [
+//   { value: 'a', children: [...] },
+//   { value: 'b', children: [...] },
+//   { value: 'c', children: [...] }
+// ]
+```
+
+-----------
+
 The following npm package may be included in this product:
 
  - babel-plugin-macros@3.1.0
@@ -1624,10 +2061,41 @@ SOFTWARE.
 
 -----------
 
+The following npm package may be included in this product:
+
+ - classnames@2.5.1
+
+This package contains the following license and notice below:
+
+The MIT License (MIT)
+
+Copyright (c) 2018 Jed Watson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+-----------
+
 The following npm packages may be included in this product:
 
  - clsx@1.2.1
  - clsx@2.0.0
+ - clsx@2.1.0
 
 These packages each contain the following license and notice below:
 
@@ -1721,6 +2189,37 @@ SOFTWARE.
 
 -----------
 
+The following npm packages may be included in this product:
+
+ - compute-scroll-into-view@3.1.0
+ - scroll-into-view-if-needed@3.1.0
+
+These packages each contain the following license and notice below:
+
+MIT License
+
+Copyright (c) 2023 Cody Olsen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+-----------
+
 The following npm package may be included in this product:
 
  - convert-source-map@1.9.0
@@ -1753,6 +2252,36 @@ OTHER DEALINGS IN THE SOFTWARE.
 
 -----------
 
+The following npm package may be included in this product:
+
+ - copy-to-clipboard@3.3.3
+
+This package contains the following license and notice below:
+
+MIT License
+
+Copyright (c) 2017 sudodoki <smd.deluzion@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+-----------
+
 The following npm package may be included in this product:
 
  - cosmiconfig@7.1.0
@@ -1813,7 +2342,7 @@ SOFTWARE.
 
 The following npm package may be included in this product:
 
- - csstype@3.1.2
+ - csstype@3.1.3
 
 This package contains the following license and notice below:
 
@@ -1841,13 +2370,13 @@ SOFTWARE.
 
 The following npm package may be included in this product:
 
- - deepmerge@2.2.1
+ - dayjs@1.11.10
 
 This package contains the following license and notice below:
 
-The MIT License (MIT)
+MIT License
 
-Copyright (c) 2012 Nicholas Fisher
+Copyright (c) 2018-present, iamkun
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -1856,16 +2385,16 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:
 
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
 
 -----------
 
@@ -2132,76 +2661,6 @@ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR I
 
 -----------
 
-The following npm package may be included in this product:
-
- - formik@2.4.3
-
-This package contains the following license and notice below:
-
-<p align="center">
-  <img src="https://user-images.githubusercontent.com/4060187/61057426-4e5a4600-a3c3-11e9-9114-630743e05814.png" width="211" height="182" alt="Formik.js" />
-</p>
-
-<h3 align="center">
-  Build forms in React, without the tears.
-</h3>
-
-<br>
-
-[![Stable Release](https://img.shields.io/npm/v/formik.svg)](https://npm.im/formik)
-[![Blazing Fast](https://badgen.now.sh/badge/speed/blazing%20%F0%9F%94%A5/green)](https://npm.im/formik)
-[![gzip size](http://img.badgesize.io/https://unpkg.com/formik@latest/dist/formik.esm.js?compression=gzip)](https://unpkg.com/formik@latest/dist/formik.esm.js)
-[![license](https://badgen.now.sh/badge/license/Apache-2.0)](./LICENSE)
-[![Discord](https://img.shields.io/discord/769256827007139912.svg?style=flat-square)](https://discord.gg/pJSg287)
-
-**Visit [https://formik.org](https://formik.org) to get started with Formik.**
-
-## Organizations and projects using Formik
-
-[List of organizations and projects using Formik](https://github.com/jaredpalmer/formik/issues/87)
-
-## Authors
-
-- Jared Palmer [@jaredpalmer](https://twitter.com/jaredpalmer)
-- Ian White [@eonwhite](https://twitter.com/eonwhite)
-
-## Contributing
-
-This monorepo uses `yarn`, so to start you'll need the package manager installed.
-
-To run E2E tests you'll also need Playwright set up, which can be done locally via `npx playwright install`. Afterward, run `yarn start:app` and in a separate tab run `yarn e2e:ui` to boot up the test runner.
-
-When you're done with your changes, we use [changesets](https://github.com/changesets/changesets) to manage release notes. Run `yarn changeset` to autogenerate notes to be appended to your pull request.
-
-Thank you!
-
-## Contributors
-
-Formik is made with <3 thanks to these wonderful people
-([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
-
-<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
-<!-- prettier-ignore -->
-| [<img src="https://avatars2.githubusercontent.com/u/4060187?v=4" width="100px;"/><br /><sub><b>Jared Palmer</b></sub>](http://jaredpalmer.com)<br />[💬](#question-jaredpalmer "Answering Questions") [💻](https://github.com/jaredpalmer/formik/commits?author=jaredpalmer "Code") [🎨](#design-jaredpalmer "Design") [📖](https://github.com/jaredpalmer/formik/commits?author=jaredpalmer "Documentation") [💡](#example-jaredpalmer "Examples") [🤔](#ideas-jaredpalmer "Ideas, Planning, & Feedback") [👀](#review-jaredpalmer "Reviewed Pull Requests") [⚠️](https://github.com/jaredpalmer/formik/commits?author=jaredpalmer "Tests") | [<img src="https://avatars0.githubusercontent.com/u/109324?v=4" width="100px;"/><br /><sub><b>Ian White</b></sub>](https://www.stardog.io)<br />[💬](#question-eonwhite "Answering Questions") [🐛](https://github.com/jaredpalmer/formik/issues?q=author%3Aeonwhite "Bug reports") [💻](https://github.com/jaredpalmer/formik/commits?author=eonwhite "Code") [📖](https://github.com/jaredpalmer/formik/commits?author=eonwhite "Documentation") [🤔](#ideas-eonwhite "Ideas, Planning, & Feedback") [👀](#review-eonwhite "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/829963?v=4" width="100px;"/><br /><sub><b>Andrej Badin</b></sub>](http://andrejbadin.com)<br />[💬](#question-Andreyco "Answering Questions") [🐛](https://github.com/jaredpalmer/formik/issues?q=author%3AAndreyco "Bug reports") [📖](https://github.com/jaredpalmer/formik/commits?author=Andreyco "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/91115?v=4" width="100px;"/><br /><sub><b>Adam Howard</b></sub>](http://adz.co.de)<br />[💬](#question-skattyadz "Answering Questions") [🐛](https://github.com/jaredpalmer/formik/issues?q=author%3Askattyadz "Bug reports") [🤔](#ideas-skattyadz "Ideas, Planning, & Feedback") [👀](#review-skattyadz "Reviewed Pull Requests") | [<img src="https://avatars1.githubusercontent.com/u/6711845?v=4" width="100px;"/><br /><sub><b>Vlad Shcherbin</b></sub>](https://github.com/VladShcherbin)<br />[💬](#question-VladShcherbin "Answering Questions") [🐛](https://github.com/jaredpalmer/formik/issues?q=author%3AVladShcherbin "Bug reports") [🤔](#ideas-VladShcherbin "Ideas, Planning, & Feedback") | [<img src="https://avatars3.githubusercontent.com/u/383212?v=4" width="100px;"/><br /><sub><b>Brikou CARRE</b></sub>](https://github.com/brikou)<br />[🐛](https://github.com/jaredpalmer/formik/issues?q=author%3Abrikou "Bug reports") [📖](https://github.com/jaredpalmer/formik/commits?author=brikou "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/5314713?v=4" width="100px;"/><br /><sub><b>Sam Kvale</b></sub>](http://skvale.github.io)<br />[🐛](https://github.com/jaredpalmer/formik/issues?q=author%3Askvale "Bug reports") [💻](https://github.com/jaredpalmer/formik/commits?author=skvale "Code") [⚠️](https://github.com/jaredpalmer/formik/commits?author=skvale "Tests") |
-| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
-| [<img src="https://avatars0.githubusercontent.com/u/13765558?v=4" width="100px;"/><br /><sub><b>Jon Tansey</b></sub>](http://jon.tansey.info)<br />[🐛](https://github.com/jaredpalmer/formik/issues?q=author%3Ajontansey "Bug reports") [💻](https://github.com/jaredpalmer/formik/commits?author=jontansey "Code") | [<img src="https://avatars0.githubusercontent.com/u/6819171?v=4" width="100px;"/><br /><sub><b>Tyler Martinez</b></sub>](http://slightlytyler.com)<br />[🐛](https://github.com/jaredpalmer/formik/issues?q=author%3Aslightlytyler "Bug reports") [📖](https://github.com/jaredpalmer/formik/commits?author=slightlytyler "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1285032?v=4" width="100px;"/><br /><sub><b>Tobias Lohse</b></sub>](http://MrLoh.se)<br />[🐛](https://github.com/jaredpalmer/formik/issues?q=author%3AMrLoh "Bug reports") [💻](https://github.com/jaredpalmer/formik/commits?author=MrLoh "Code") |
-
-<!-- ALL-CONTRIBUTORS-LIST:END -->
-
-This project follows the
-[all-contributors](https://github.com/kentcdodds/all-contributors)
-specification. Contributions of any kind welcome!
-
-## Related
-
-- [TSDX](https://github.com/jaredpalmer/tsdx) - Zero-config CLI for TypeScript used by this repo. (Formik's Rollup configuration as a CLI)
-
----
-
-[Apache 2.0 License.](https://github.com/jaredpalmer/formik/blob/master/LICENSE)
-
------------
-
 The following npm package may be included in this product:
 
  - free-style@3.1.0
@@ -2234,35 +2693,7 @@ THE SOFTWARE.
 
 The following npm package may be included in this product:
 
- - function-bind@1.1.1
-
-This package contains the following license and notice below:
-
-Copyright (c) 2013 Raynos.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
------------
-
-The following npm package may be included in this product:
-
- - has@1.0.3
+ - has@1.0.4
 
 This package contains the following license and notice below:
 
@@ -2827,8 +3258,8 @@ THE SOFTWARE.
 
 The following npm packages may be included in this product:
 
- - lib0@0.2.83
- - y-protocols@1.0.5
+ - lib0@0.2.87
+ - y-protocols@1.0.6
 
 These packages each contain the following license and notice below:
 
@@ -3047,12 +3478,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
 
 -----------
 
-The following npm packages may be included in this product:
+The following npm package may be included in this product:
 
  - memoize-one@5.2.1
- - tiny-warning@1.0.3
 
-These packages each contain the following license and notice below:
+This package contains the following license and notice below:
 
 MIT License
 
@@ -3308,8 +3738,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 The following npm packages may be included in this product:
 
- - property-expr@2.0.5
- - yup@1.2.0
+ - property-expr@2.0.6
+ - yup@1.3.2
 
 These packages each contain the following license and notice below:
 
@@ -3339,44 +3769,250 @@ SOFTWARE.
 
 The following npm package may be included in this product:
 
- - punycode@2.3.0
+ - punycode@2.3.0
+
+This package contains the following license and notice below:
+
+Copyright Mathias Bynens <https://mathiasbynens.be/>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm package may be included in this product:
+
+ - qrcode.react@3.1.0
+
+This package contains the following license and notice below:
+
+ISC License
+
+Copyright (c) 2015, Paul O’Shannessy
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+
+This product bundles QR Code Generator, which is available under a
+"MIT" license. For details, see src/third-party/qrcodegen.
+
+-----------
+
+The following npm packages may be included in this product:
+
+ - querystringify@2.2.0
+ - requires-port@1.0.0
+ - url-parse@1.5.10
+
+These packages each contain the following license and notice below:
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Unshift.io, Arnout Kazemier,  the Contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+-----------
+
+The following npm packages may be included in this product:
+
+ - rc-cascader@3.21.2
+ - rc-select@14.11.0
+
+These packages each contain the following license and notice below:
+
+The MIT License (MIT)
+
+Copyright (c) 2014-present alipay.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm package may be included in this product:
+
+ - rc-checkbox@3.1.0
+
+This package contains the following license and notice below:
+
+The MIT License (MIT) Copyright (c) 2016 React Components
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm packages may be included in this product:
+
+ - rc-drawer@7.0.0
+ - rc-image@7.5.1
+ - rc-tree-select@5.17.0
+ - rc-tree@5.8.5
+
+These packages each contain the following license and notice below:
+
+MIT LICENSE
+
+Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm package may be included in this product:
+
+ - rc-table@7.39.0
+
+This package contains the following license and notice below:
+
+MIT LICENSE
+
+Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm packages may be included in this product:
+
+ - rc-tooltip@6.1.3
+ - rc-virtual-list@3.11.4
+
+These packages each contain the following license and notice below:
+
+The MIT License (MIT)
+
+Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+-----------
+
+The following npm package may be included in this product:
+
+ - rc-upload@4.5.2
 
 This package contains the following license and notice below:
 
-Copyright Mathias Bynens <https://mathiasbynens.be/>
+The MIT License (MIT)
 
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
+Copyright (c) 2016-present react-component
 
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 -----------
 
-The following npm packages may be included in this product:
+The following npm package may be included in this product:
 
- - querystringify@2.2.0
- - requires-port@1.0.0
- - url-parse@1.5.10
+ - rc-util@5.38.2
 
-These packages each contain the following license and notice below:
+This package contains the following license and notice below:
 
 The MIT License (MIT)
 
-Copyright (c) 2015 Unshift.io, Arnout Kazemier,  the Contributors.
+Copyright (c) 2014-present yiminghe
+Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -3385,16 +4021,16 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 copies of the Software, and to permit persons to whom the Software is
 furnished to do so, subject to the following conditions:
 
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 -----------
 
@@ -4203,32 +4839,30 @@ SOFTWARE.
 
 The following npm package may be included in this product:
 
- - react-fast-compare@2.0.4
+ - react-js-cron@5.0.1
 
 This package contains the following license and notice below:
 
-MIT License
+The MIT License (MIT)
 
-Copyright (c) 2018 Formidable Labs
-Copyright (c) 2017 Evgeny Poberezkin
+Copyright (c) Xavier Rutayisire
 
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 -----------
 
@@ -4483,7 +5117,37 @@ THE SOFTWARE.
 
 The following npm package may be included in this product:
 
- - resolve@1.22.4
+ - resize-observer-polyfill@1.5.1
+
+This package contains the following license and notice below:
+
+The MIT License (MIT)
+
+Copyright (c) 2016 Denis Rul
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+-----------
+
+The following npm package may be included in this product:
+
+ - resolve@1.22.8
 
 This package contains the following license and notice below:
 
@@ -4566,10 +5230,41 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 The following npm package may be included in this product:
 
- - stylis@4.2.0
+ - string-convert@0.2.1
 
 This package contains the following license and notice below:
 
+The MIT License (MIT)
+
+Copyright (c) 2014 Kiran Abburi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+-----------
+
+The following npm packages may be included in this product:
+
+ - stylis@4.2.0
+ - stylis@4.3.1
+
+These packages each contain the following license and notice below:
+
 MIT License
 
 Copyright (c) 2016-present Sultan Tarimo
@@ -4624,6 +5319,340 @@ SOFTWARE.
 
 -----------
 
+The following npm package may be included in this product:
+
+ - throttle-debounce@5.0.0
+
+This package contains the following license and notice below:
+
+Copyright (c) Ivan Nikolić <http://ivannikolic.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+---
+
+Copyright (c) 2010 "Cowboy" Ben Alman
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+---
+
+        GNU GENERAL PUBLIC LICENSE
+           Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+          Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+        GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+          NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+-----------
+
 The following npm package may be included in this product:
 
  - tiny-case@1.0.3
@@ -4678,6 +5707,33 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
 
 -----------
 
+The following npm package may be included in this product:
+
+ - toggle-selection@1.0.6
+
+This package contains the following license and notice below:
+
+# Toggle Selection
+
+Simple module exposing function that deselects current browser selection and returns function that restores selection.
+
+```
+var deselect = require('toggle-selection');
+var reselect = deselect(); // remove all selection
+// … 
+// do something with current selection, text, etc;
+// …
+reselect(); // restore selection
+```
+
+All credits go to [@shvaikalesh](https://github.com/shvaikalesh).
+
+# [Example](https://github.com/sudodoki/toggle-selection/blob/master/example)
+
+![example recording](http://g.recordit.co/YPu6mHvcKe.gif)
+
+-----------
+
 The following npm package may be included in this product:
 
  - toposort@2.0.2
@@ -4707,27 +5763,6 @@ THE SOFTWARE.
 
 -----------
 
-The following npm package may be included in this product:
-
- - tslib@2.6.2
-
-This package contains the following license and notice below:
-
-Copyright (c) Microsoft Corporation.
-
-Permission to use, copy, modify, and/or distribute this software for any
-purpose with or without fee is hereby granted.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
-REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
-INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
-OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-PERFORMANCE OF THIS SOFTWARE.
-
------------
-
 The following npm package may be included in this product:
 
  - type-fest@2.19.0
@@ -5672,6 +6707,53 @@ SOFTWARE.
 
 -----------
 
+The following npm package may be included in this product:
+
+ - tzdata@1.0.40
+
+This package contains the following license and notice below:
+
+The MIT License (MIT)
+[OSI Approved License]
+The MIT License (MIT)
+
+Copyright (c) 2016 Rogier Schouten <https://github.com/rogierschouten>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+-----------
+
+The following npm package may be included in this product:
+
+ - undici-types@5.25.3
+
+This package contains the following license and notice below:
+
+# undici-types
+
+This package is a dual-publish of the [undici](https://www.npmjs.com/package/undici) library types. The `undici` package **still contains types**. This package is for users who _only_ need undici types (such as for `@types/node`). It is published alongside every release of `undici`, so you can always use the same version.
+
+- [GitHub nodejs/undici](https://github.com/nodejs/undici)
+- [Undici Documentation](https://undici.nodejs.org/#/)
+
+-----------
+
 The following npm package may be included in this product:
 
  - uri-js@4.4.1
@@ -5756,7 +6838,7 @@ SOFTWARE.
 
 The following npm package may be included in this product:
 
- - ws@8.13.0
+ - ws@8.14.2
 
 This package contains the following license and notice below:
 
@@ -5807,7 +6889,7 @@ THIS SOFTWARE.
 
 The following npm package may be included in this product:
 
- - yjs@13.6.7
+ - yjs@13.6.8
 
 This package contains the following license and notice below:
 


Test patch:
diff --git a/dataproc_jupyter_plugin/tests/test_handlers.py b/dataproc_jupyter_plugin/tests/test_handlers.py
index a64912cd..d6c42c3c 100644
--- a/dataproc_jupyter_plugin/tests/test_handlers.py
+++ b/dataproc_jupyter_plugin/tests/test_handlers.py
@@ -22,6 +22,4 @@ async def test_get_example(jp_fetch):
     # Then
     assert response.code == 200
     payload = json.loads(response.body)
-    assert payload == {
-        "data": "This is /dataproc-plugin/get-example endpoint!"
-    }
+    assert payload == {"data": "This is /dataproc-plugin/get-example endpoint!"}