diff --git a/.gitignore b/.gitignore index 7bf6e43..38193a1 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ /pcs-web-ui-node-modules-0.1.19.tar.xz /pcs-5b7d498915e0cc876b29fe9ebd709c061ac754db.tar.gz /pcs-0.12.0a1.tar.gz +/pcs-web-ui-0.1.20.tar.gz +/pcs-web-ui-node-modules-0.1.20.tar.xz diff --git a/fix-grammatical-error.patch b/fix-grammatical-error.patch deleted file mode 100644 index 9f4786a..0000000 --- a/fix-grammatical-error.patch +++ /dev/null @@ -1,30 +0,0 @@ -From ead22ee414ba35546701d6bb85504af12fd6501e Mon Sep 17 00:00:00 2001 -From: Michal Pospisil -Date: Tue, 28 May 2024 16:06:53 +0200 -Subject: [PATCH] fix grammatical error - ---- - .../app/view/cluster/share/utilization/UtilizationView.tsx | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - -diff --git a/packages/app/src/app/view/cluster/share/utilization/UtilizationView.tsx b/packages/app/src/app/view/cluster/share/utilization/UtilizationView.tsx -index 3b140975..a8c7494c 100644 ---- a/packages/app/src/app/view/cluster/share/utilization/UtilizationView.tsx -+++ b/packages/app/src/app/view/cluster/share/utilization/UtilizationView.tsx -@@ -29,11 +29,11 @@ export const UtilizationView = (props: { - ).toLowerCase() === "default" && ( - -

-- Utilization attributes has no effect because the cluster -+ Utilization attributes have no effect because the cluster - property placement-strategy is - {clusterProperties["placement-strategy"] - ? " set to value default. " --- -2.45.1 - diff --git a/fix-tests-for-missing-webui.patch b/fix-tests-for-missing-webui.patch new file mode 100644 index 0000000..d88f9ea --- /dev/null +++ b/fix-tests-for-missing-webui.patch @@ -0,0 +1,444 @@ +From 525b102bd053a6653dff663845c98683fa78bbab Mon Sep 17 00:00:00 2001 +From: Ivan Devat +Date: Mon, 24 Jun 2024 17:19:58 +0200 +Subject: [PATCH] fix tests for missing webui + +--- + pcs_test/Makefile.am | 1 + + pcs_test/smoke.sh.in | 23 ++++++-- + pcs_test/tier0/daemon/app/fixtures_app.py | 53 +++---------------- + .../tier0/daemon/app/fixtures_app_webui.py | 28 ++++++++++ + pcs_test/tier0/daemon/app/test_app_gui.py | 37 ++++++++++--- + pcs_test/tier0/daemon/app/test_app_spa.py | 26 +++++++-- + pcs_test/tier0/daemon/test_env.py | 6 ++- + pcs_test/tier0/daemon/test_session.py | 25 ++++++--- + pcs_test/tools/misc.py | 9 ++++ + 9 files changed, 138 insertions(+), 70 deletions(-) + create mode 100644 pcs_test/tier0/daemon/app/fixtures_app_webui.py + +diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am +index 4e71fcda..6c5932b3 100644 +--- a/pcs_test/Makefile.am ++++ b/pcs_test/Makefile.am +@@ -134,6 +134,7 @@ EXTRA_DIST = \ + tier0/common/test_tools.py \ + tier0/common/test_tools_xml_fromstring.py \ + tier0/daemon/app/fixtures_app.py \ ++ tier0/daemon/app/fixtures_app_webui.py \ + tier0/daemon/app/__init__.py \ + tier0/daemon/app/test_api_v0.py \ + tier0/daemon/app/test_api_v1.py \ +diff --git a/pcs_test/smoke.sh.in b/pcs_test/smoke.sh.in +index a4b3ac71..37eac345 100755 +--- a/pcs_test/smoke.sh.in ++++ b/pcs_test/smoke.sh.in +@@ -66,10 +66,25 @@ cat < ${pcsd_settings_conf_path} + EOF + cat ${pcsd_settings_conf_path} + pcs cluster start --all --wait +-curl --insecure --data "username=${cluster_user}&password=${cluster_user_password}" --cookie-jar ${cookie_file} https://localhost:2224/ui/login +-curl --insecure --cookie ${cookie_file} --header "X-Requested-With: XMLHttpRequest" --data "hidden[hidden_input]=&config[stonith-enabled]=false" https://localhost:2224/managec/${cluster_name}/update_cluster_settings > "${output_file}" +-cat "${output_file}"; echo "" +-[ "$(cat ${output_file})" = "Update Successful" ] ++ ++webui_http_code_response=$( ++ curl --insecure --silent --output /dev/null --write-out "%{http_code}" \ ++ https://localhost:2224/ui/ ++) ++if [ "$webui_http_code_response" = "200" ]; then ++ # Webui backend check ++ curl --insecure --data "username=${cluster_user}&password=${cluster_user_password}" --cookie-jar ${cookie_file} https://localhost:2224/ui/login ++ curl --insecure --cookie ${cookie_file} --header "X-Requested-With: XMLHttpRequest" --data "hidden[hidden_input]=&config[stonith-enabled]=false" https://localhost:2224/managec/${cluster_name}/update_cluster_settings > "${output_file}" ++ cat "${output_file}"; echo "" ++ [ "$(cat ${output_file})" = "Update Successful" ] ++elif [ "$webui_http_code_response" = "401" ]; then ++ curl --insecure https://localhost:2224/ui/ > "${output_file}" ++ cat "${output_file}"; echo "" ++ [ "$(cat "${output_file}")" = '{"notauthorized":"true"}' ] ++else ++ echo "Unexpected response from https://localhost:2224/ui/ - http code: '${webui_http_code_response}'" ++ exit 1 ++fi + + # Sanity check of API V1 + curl -kb "token=${token}" https://localhost:2224/api/v1/resource-agent-get-agents-list/v1 --data '{}' > "${output_file}" +diff --git a/pcs_test/tier0/daemon/app/fixtures_app.py b/pcs_test/tier0/daemon/app/fixtures_app.py +index 3c0d6a4f..56acf473 100644 +--- a/pcs_test/tier0/daemon/app/fixtures_app.py ++++ b/pcs_test/tier0/daemon/app/fixtures_app.py +@@ -1,16 +1,11 @@ + from pprint import pformat + from urllib.parse import urlencode + +-from tornado.httputil import ( +- HTTPHeaders, +- parse_cookie, +-) ++from tornado.httputil import HTTPHeaders + from tornado.testing import AsyncHTTPTestCase + from tornado.web import Application + + from pcs.daemon import ruby_pcsd +-from pcs.daemon.app.webui import session +-from pcs.daemon.app.webui.auth import PCSD_SESSION + + USER = "user" + GROUPS = ["group1", "group2"] +@@ -56,6 +51,13 @@ class AppTest(AsyncHTTPTestCase): + def fetch(self, path, raise_error=False, **kwargs): + if "follow_redirects" not in kwargs: + kwargs["follow_redirects"] = False ++ ++ if "is_ajax" in kwargs: ++ if "headers" not in kwargs: ++ kwargs["headers"] = {} ++ kwargs["headers"]["X-Requested-With"] = "XMLHttpRequest" ++ del kwargs["is_ajax"] ++ + response = super().fetch(path, raise_error=raise_error, **kwargs) + # "Strict-Transport-Security" header is expected in every response + self.assertTrue( +@@ -91,45 +93,6 @@ class AppTest(AsyncHTTPTestCase): + self.assert_headers_contains(response.headers, self.wrapper.headers) + self.assertEqual(response.body, self.wrapper.body) + +- +-class AppUiTestMixin(AppTest): +- def setUp(self): +- self.session_storage = session.Storage(lifetime_seconds=10) +- super().setUp() +- +- def assert_session_in_response(self, response, sid=None): +- self.assertTrue("Set-Cookie" in response.headers) +- cookie = parse_cookie(response.headers["Set-Cookie"]) +- self.assertTrue(PCSD_SESSION, cookie) +- if sid: +- self.assertEqual(cookie[PCSD_SESSION], sid) +- return cookie[PCSD_SESSION] +- +- def fetch(self, path, raise_error=False, **kwargs): +- if "sid" in kwargs: +- if "headers" not in kwargs: +- kwargs["headers"] = {} +- kwargs["headers"]["Cookie"] = f"{PCSD_SESSION}={kwargs['sid']}" +- del kwargs["sid"] +- +- if "is_ajax" in kwargs: +- if "headers" not in kwargs: +- kwargs["headers"] = {} +- kwargs["headers"]["X-Requested-With"] = "XMLHttpRequest" +- del kwargs["is_ajax"] +- +- if "follow_redirects" not in kwargs: +- kwargs["follow_redirects"] = False +- +- return super().fetch(path, raise_error=raise_error, **kwargs) +- +- def create_login_session(self): +- return self.session_storage.login(USER) +- +- def assert_success_response(self, response, expected_body): +- self.assertEqual(response.code, 200) +- self.assertEqual(response.body.decode(), expected_body) +- + def assert_unauth_ajax(self, response): + self.assertEqual(response.code, 401) + self.assertEqual(response.body, b'{"notauthorized":"true"}') +diff --git a/pcs_test/tier0/daemon/app/fixtures_app_webui.py b/pcs_test/tier0/daemon/app/fixtures_app_webui.py +new file mode 100644 +index 00000000..e38ca3e7 +--- /dev/null ++++ b/pcs_test/tier0/daemon/app/fixtures_app_webui.py +@@ -0,0 +1,28 @@ ++try: ++ from pcs.daemon.app import webui ++except ImportError: ++ # You need to skip tests in tests that uses AppTest in the case webui ++ # is not there. ++ webui = None ++ ++from pcs_test.tier0.daemon.app import fixtures_app ++ ++ ++class AppTest(fixtures_app.AppTest): ++ def setUp(self): ++ self.session_storage = webui.session.Storage(lifetime_seconds=10) ++ super().setUp() ++ ++ def fetch(self, path, raise_error=False, **kwargs): ++ if "sid" in kwargs: ++ if "headers" not in kwargs: ++ kwargs["headers"] = {} ++ kwargs["headers"][ ++ "Cookie" ++ ] = f"{webui.auth.PCSD_SESSION}={kwargs['sid']}" ++ del kwargs["sid"] ++ ++ return super().fetch(path, raise_error=raise_error, **kwargs) ++ ++ def create_login_session(self): ++ return self.session_storage.login(fixtures_app.USER) +diff --git a/pcs_test/tier0/daemon/app/test_app_gui.py b/pcs_test/tier0/daemon/app/test_app_gui.py +index b3446fdb..2317eea5 100644 +--- a/pcs_test/tier0/daemon/app/test_app_gui.py ++++ b/pcs_test/tier0/daemon/app/test_app_gui.py +@@ -1,14 +1,24 @@ + import logging + from unittest import mock + ++from tornado.httputil import parse_cookie ++ + from pcs.daemon import ruby_pcsd + from pcs.daemon.app import sinatra_ui + from pcs.daemon.app.auth import UnixSocketAuthProvider +-from pcs.daemon.app.webui import sinatra_ui as sinatra_ui_webui ++ ++try: ++ from pcs.daemon.app import webui ++except ImportError: ++ webui = None + from pcs.lib.auth.provider import AuthProvider + from pcs.lib.auth.types import AuthUser + +-from pcs_test.tier0.daemon.app import fixtures_app ++from pcs_test.tier0.daemon.app import ( ++ fixtures_app, ++ fixtures_app_webui, ++) ++from pcs_test.tools.misc import skip_unless_webui_installed + + # Don't write errors to test output. + logging.getLogger("tornado.access").setLevel(logging.CRITICAL) +@@ -33,21 +43,28 @@ def patch_get_unix_socket_user(user): + auth_provider = AuthProvider(logging.getLogger("test logger")) + + +-class AppTest(fixtures_app.AppUiTestMixin): ++@skip_unless_webui_installed() ++@patch_login_user() ++class SinatraAjaxProtectedSession(fixtures_app_webui.AppTest): + def setUp(self): + self.wrapper = fixtures_app.RubyPcsdWrapper(ruby_pcsd.SINATRA) + super().setUp() + +- +-@patch_login_user() +-class SinatraAjaxProtectedSession(AppTest): + def get_routes(self): +- return sinatra_ui_webui.get_routes( ++ return webui.sinatra_ui.get_routes( + self.session_storage, + auth_provider, + self.wrapper, + ) + ++ def assert_session_in_response(self, response, sid=None): ++ self.assertTrue("Set-Cookie" in response.headers) ++ cookie = parse_cookie(response.headers["Set-Cookie"]) ++ self.assertTrue(webui.auth.PCSD_SESSION, cookie) ++ if sid: ++ self.assertEqual(cookie[webui.auth.PCSD_SESSION], sid) ++ return cookie[webui.auth.PCSD_SESSION] ++ + def test_deal_without_authentication(self): + self.assert_unauth_ajax(self.get("/some-ajax", is_ajax=True)) + +@@ -63,7 +80,11 @@ class SinatraAjaxProtectedSession(AppTest): + + + @patch_login_user() +-class SinatraAjaxProtectedUnixSocket(AppTest): ++class SinatraAjaxProtectedUnixSocket(fixtures_app.AppTest): ++ def setUp(self): ++ self.wrapper = fixtures_app.RubyPcsdWrapper(ruby_pcsd.SINATRA) ++ super().setUp() ++ + def get_routes(self): + return sinatra_ui.get_routes(auth_provider, self.wrapper) + +diff --git a/pcs_test/tier0/daemon/app/test_app_spa.py b/pcs_test/tier0/daemon/app/test_app_spa.py +index 89a5ab0e..6acf3c7d 100644 +--- a/pcs_test/tier0/daemon/app/test_app_spa.py ++++ b/pcs_test/tier0/daemon/app/test_app_spa.py +@@ -2,12 +2,22 @@ import logging + import os + from unittest import mock + +-from pcs.daemon.app import webui ++try: ++ from pcs.daemon.app import webui ++except ImportError: ++ webui = None ++ + from pcs.lib.auth.provider import AuthProvider + from pcs.lib.auth.types import AuthUser + +-from pcs_test.tier0.daemon.app import fixtures_app +-from pcs_test.tools.misc import get_tmp_dir ++from pcs_test.tier0.daemon.app import ( ++ fixtures_app, ++ fixtures_app_webui, ++) ++from pcs_test.tools.misc import ( ++ get_tmp_dir, ++ skip_unless_webui_installed, ++) + + LOGIN_BODY = {"username": fixtures_app.USER, "password": fixtures_app.PASSWORD} + PREFIX = "/ui/" +@@ -16,7 +26,7 @@ PREFIX = "/ui/" + logging.getLogger("tornado.access").setLevel(logging.CRITICAL) + + +-class AppTest(fixtures_app.AppUiTestMixin): ++class AppTest(fixtures_app_webui.AppTest): + def setUp(self): + self.public_dir = get_tmp_dir("tier0_daemon_app_spa") + self.spa_dir_path = os.path.join(self.public_dir.name, "ui") +@@ -41,7 +51,12 @@ class AppTest(fixtures_app.AppUiTestMixin): + auth_provider=AuthProvider(logging.Logger("test logger")), + ) + ++ def assert_success_response(self, response, expected_body): ++ self.assertEqual(response.code, 200) ++ self.assertEqual(response.body.decode(), expected_body) ++ + ++@skip_unless_webui_installed() + class Static(AppTest): + def test_index(self): + self.assert_success_response( +@@ -50,6 +65,7 @@ class Static(AppTest): + ) + + ++@skip_unless_webui_installed() + class Fallback(AppTest): + def setUp(self): + super().setUp() +@@ -65,6 +81,7 @@ class Fallback(AppTest): + ) + + ++@skip_unless_webui_installed() + class Login(AppTest): + def setUp(self): + super().setUp() +@@ -94,6 +111,7 @@ class Login(AppTest): + ) + + ++@skip_unless_webui_installed() + class Logout(AppTest): + def test_can_logout(self): + session1 = self.create_login_session() +diff --git a/pcs_test/tier0/daemon/test_env.py b/pcs_test/tier0/daemon/test_env.py +index 40f0c402..0eff3660 100644 +--- a/pcs_test/tier0/daemon/test_env.py ++++ b/pcs_test/tier0/daemon/test_env.py +@@ -8,7 +8,10 @@ from unittest import ( + from pcs import settings + from pcs.daemon import env + +-from pcs_test.tools.misc import create_setup_patch_mixin ++from pcs_test.tools.misc import ( ++ create_setup_patch_mixin, ++ skip_unless_webui_installed, ++) + + + def webui_fallback(public_dir): +@@ -177,6 +180,7 @@ class Prepare(TestCase, create_setup_patch_mixin(env)): + {env.PCSD_DEBUG: "false"} + ) + ++ @skip_unless_webui_installed() + def test_errors_on_missing_paths(self): + self.path_exists.return_value = False + self.assert_environ_produces_modified_pcsd_env( +diff --git a/pcs_test/tier0/daemon/test_session.py b/pcs_test/tier0/daemon/test_session.py +index a893c01d..e8eff4cd 100644 +--- a/pcs_test/tier0/daemon/test_session.py ++++ b/pcs_test/tier0/daemon/test_session.py +@@ -1,23 +1,31 @@ + from contextlib import contextmanager + from unittest import TestCase + +-from pcs.daemon.app.webui import session +-from pcs.daemon.app.webui.session import Session ++try: ++ from pcs.daemon.app import webui ++except ImportError: ++ webui = None + +-from pcs_test.tools.misc import create_setup_patch_mixin ++from pcs_test.tools.misc import ( ++ create_setup_patch_mixin, ++ skip_unless_webui_installed, ++) + + SID = "abc" + USER = "user" + GROUPS = ["group1", "group2"] + + +-PatchSessionMixin = create_setup_patch_mixin(session) ++# If webui is None, the tests in this file using these mixins are skipped, ++# passing a dummy object instead to make this executable ++PatchSessionMixin = create_setup_patch_mixin(webui) if webui else object + + ++@skip_unless_webui_installed() + class SessionTest(TestCase, PatchSessionMixin): + def setUp(self): +- self.now = self.setup_patch("now", return_value=0) +- self.session = Session(SID, USER) ++ self.now = self.setup_patch("session.now", return_value=0) ++ self.session = webui.session.Session(SID, USER) + + def test_session_grows_older(self): + self.now.return_value = 10.1 +@@ -42,10 +50,11 @@ class SessionTest(TestCase, PatchSessionMixin): + session1.sid + + ++@skip_unless_webui_installed() + class StorageTest(TestCase, PatchSessionMixin): + def setUp(self): +- self.now = self.setup_patch("now", return_value=0) +- self.storage = session.Storage(lifetime_seconds=10) ++ self.now = self.setup_patch("session.now", return_value=0) ++ self.storage = webui.session.Storage(lifetime_seconds=10) + + def test_does_not_accept_foreign_sid(self): + self.assertIsNone(self.storage.get("unknown_sid")) +diff --git a/pcs_test/tools/misc.py b/pcs_test/tools/misc.py +index 28e03411..7a048a4d 100644 +--- a/pcs_test/tools/misc.py ++++ b/pcs_test/tools/misc.py +@@ -15,6 +15,11 @@ from pcs.cli.common.parse_args import InputModifiers + from pcs.common import str_tools + from pcs.lib.external import CommandRunner + ++try: ++ from pcs.daemon.app import webui ++except ImportError: ++ webui = None # type: ignore ++ + from pcs_test import TEST_ROOT + from pcs_test import settings as tests_settings + from pcs_test.tools.custom_mock import MockLibraryReportProcessor +@@ -292,6 +297,10 @@ def skip_unless_booth_resource_agent_installed(): + ) + + ++def skip_unless_webui_installed(): ++ return skipUnless(webui, "test requires webui which is not installed") ++ ++ + def create_patcher(target_prefix_or_module): + """ + Return function for patching tests with preconfigured target prefix +-- +2.45.2 + diff --git a/pcs.spec b/pcs.spec index df76bc4..62a880e 100644 --- a/pcs.spec +++ b/pcs.spec @@ -1,6 +1,6 @@ Name: pcs Version: 0.12.0~a1 -Release: 2%{?dist} +Release: 3%{?dist} # https://docs.fedoraproject.org/en-US/packaging-guidelines/LicensingGuidelines/ # https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Good_Licenses # GPL-2.0-only: pcs @@ -27,8 +27,8 @@ ExclusiveArch: x86_64 s390x ppc64le aarch64 %global pcs_source_name %{name}-%{version_or_commit} # ui_commit can be determined by hash, tag or branch -%global ui_commit 0.1.19 -%global ui_modules_version 0.1.19 +%global ui_commit 0.1.20 +%global ui_modules_version 0.1.20 %global ui_src_name pcs-web-ui-%{ui_commit} %global dacite_version 1.8.1 @@ -117,10 +117,10 @@ Source101: https://github.com/ClusterLabs/pcs-web-ui/releases/download/%{ui_comm # pcs patches: <= 200 # Patch1: name.patch Patch1: do-not-support-cluster-setup-with-udp-u-transport.patch +Patch2: fix-tests-for-missing-webui.patch # ui patches: >200 # Patch201: name-web-ui.patch -Patch201: fix-grammatical-error.patch # git for patches @@ -335,13 +335,13 @@ update_times_patch(){ %autosetup -D -T -b 100 -a 101 -S git -n %{ui_src_name} -N %autopatch -p1 -m 201 # update_times_patch %%{PATCH201} -update_times_patch %{PATCH201} # patch pcs sources %autosetup -S git -n %{pcs_source_name} -N %autopatch -p1 -M 200 # update_times_patch %%{PATCH1} update_times_patch %{PATCH1} +update_times_patch %{PATCH2} # generate .tarball-version if building from an untagged commit, not a released version # autogen uses git-version-gen which uses .tarball-version for generating version number @@ -480,6 +480,11 @@ rm -rf $RPM_BUILD_ROOT%{_libdir}/%{rubygem_bundle_dir}/gems/ffi-%{version_rubyge rm -rf $RPM_BUILD_ROOT%{_libdir}/%{rubygem_bundle_dir}/gems/nio4r-%{version_rubygem_nio4r}/ext rm -rf $RPM_BUILD_ROOT%{_libdir}/%{rubygem_bundle_dir}/gems/puma-%{version_rubygem_puma}/ext +# Sinatra contains example applications which are unnecessary (discovered by brp_mangle_shebangs) +rm -rf $RPM_BUILD_ROOT%{_libdir}/%{rubygem_bundle_dir}/gems/sinatra-%{version_rubygem_sinatra}/examples + +# Puma contains an unnecessary rc init script (discovered by brp_mangle_shebangs) +rm -rf $RPM_BUILD_ROOT%{_libdir}/%{rubygem_bundle_dir}/gems/puma-%{version_rubygem_puma}/docs/jungle/rc.d %check # Run validation of cockpit metainfo @@ -633,6 +638,11 @@ run_all_tests %changelog +* Wed Jul 10 2024 Michal Pospíšil - 0.12.0~a1-3 +- Since 0.12.0~a1-1, pcs-web-ui is no longer a standalone application running on port 2224. It is now accessible through RHEL web console after installing the HA Cluster Management add-on (cockpit and cockpit-ha-cluster packages, pcsd must be running) +- Updated pcs-web-ui to 0.1.20 + Resolves: RHEL-34783 + * Mon Jun 24 2024 Troy Dawson - 0.12.0~a1-2 - Bump release for June 2024 mass rebuild diff --git a/sources b/sources index c716f50..4133e86 100644 --- a/sources +++ b/sources @@ -19,6 +19,6 @@ SHA512 (rackup-2.1.0.gem) = f2b66902b04ddce5ca9389822118244c591b5c83766a55aee885 SHA512 (sinatra-4.0.0.gem) = 1eb8c6e8966461d3fa463b5c87e8bc3cd58243fc997a104671e252b866bb653dfc16d7b9f677e016ae91cb30998d72f8778eb2b2254ce27cf304944a6bfa8c05 SHA512 (webrick-1.8.1.gem) = 3bf45e3a52190dccaa6e883923448b745a420eff2a1533eacdd2aed0e4c67f5c6d813c85606f8fc12952c004e4984fd97ebc3c361a42b49cebe5b84c8fc6e99d SHA512 (tornado-6.4.0.tar.gz) = dc584acc14d93c7109e4744b690641ae318ee9ad2c42a4c3560c315fa8654de3a64574c7187f5afdbde2906b7cccf5725f45462e710effb6f025e5ec1a3810d4 -SHA512 (pcs-web-ui-0.1.19.tar.gz) = 3aa407f2e90f236324949d33f6ee4701c1d4898deeed2fa0d3367f9a1a0d0687e44b966c106ff772e450c51be36abd3f17656432b5da4d0c9f1a80e6f80866c6 -SHA512 (pcs-web-ui-node-modules-0.1.19.tar.xz) = 206658b964a3b1a6c7c4b171d6686ef9097ee7c36720088cdc220c50e44d09d58fb4d1aac8e2eb7bb44267f581b666d38b3148352f2f235b477e8a7ab7ee8551 SHA512 (pcs-0.12.0a1.tar.gz) = 1c455dcb24bc6ab0cac0c879f932d54ad8b36c0a937557a1e873ad9442e85ff81cae23c57236c6421cc5d1c36027d583b987e21a4ad80321728090498ef8f30c +SHA512 (pcs-web-ui-0.1.20.tar.gz) = 83117eafbe694b133228dfd8d7de30a7cc58693b8844f7d1af9c12c02618a7af8dd10c374b56c3e722d84c5691488f25bfc3389f22954935a2e6dd42b16c7b2c +SHA512 (pcs-web-ui-node-modules-0.1.20.tar.xz) = 94cd6a306a6784c3decc351b1469efe98beadd43efbfa7848ccc6146be32f704281432669589e2c666b431a32bd6caf101fa08fb05deaab2b58a8cda8b40b85d