Repository: Kinto/kinto

Issue:
Should we be including the entire source for kinto-admin in this repo?
It seems like a compiled output from another project. Can we just build it as part of the distribution for releases instead, or something like that?
Should we be including the entire source for kinto-admin in this repo?
It seems like a compiled output from another project. Can we just build it as part of the distribution for releases instead, or something like that?


Discussion:
The kinto-admin JS is considered as being to some static files dependencies of the admin plugin.

If you consider master as being a runnable distribution of Kinto, then we are already doing that.
We update it in master for each release of the kinto-admin.
We can update the static files to there last version by running `make update-kinto-admin`

After using this process for few months I find it really robust and easy , why do you think we should change it?


For the same reason that we don't commit .pyc files to master. These files are not the preferred form for modification or even readable. Verifying a PR that modifies these files is therefore impossible. These files are the output of an automatic process and so keeping them around is just wasted space. It probably wouldn't be such a big deal to me except that the kinto-admin plugin is by far the heaviest thing in a git clone, weighing in at 7MB (out of 7.9MB), so doing a `git pull` is much slower when someone updates these files.

I agree that master should be "runnable", but I don't think that means we need to keep kinto-admin build outputs in source control. We could agree that "runnable" means "runnable without the admin", or we could agree that "runnable" means "runnable with everything in a non-optimized way", or we could agree that "runnable" means "runnable once you execute a build step". Each of these ways could permit kinto-admin build outputs to not be checked in to git, because they don't belong there.
I see your point, that something we need to discuss and fix then.
Runnable means `make serve` just works. We could add a Makefile entry to fix that.

I agree that it is very annoying to have to files (eg. `git grep`)
I just found that zest.releaser has the notion of hooks:
https://github.com/zestsoftware/zest.releaser/blob/6.8.1/zest/releaser/tests/functional-with-hooks.txt

So that means we could have a release hook that builds the admin before pushing it to pypi. 

From a git clone, a `make` command would be enough.
The kinto-admin JS is considered as being to some static files dependencies of the admin plugin.

If you consider master as being a runnable distribution of Kinto, then we are already doing that.
We update it in master for each release of the kinto-admin.
We can update the static files to there last version by running `make update-kinto-admin`

After using this process for few months I find it really robust and easy , why do you think we should change it?


For the same reason that we don't commit .pyc files to master. These files are not the preferred form for modification or even readable. Verifying a PR that modifies these files is therefore impossible. These files are the output of an automatic process and so keeping them around is just wasted space. It probably wouldn't be such a big deal to me except that the kinto-admin plugin is by far the heaviest thing in a git clone, weighing in at 7MB (out of 7.9MB), so doing a `git pull` is much slower when someone updates these files.

I agree that master should be "runnable", but I don't think that means we need to keep kinto-admin build outputs in source control. We could agree that "runnable" means "runnable without the admin", or we could agree that "runnable" means "runnable with everything in a non-optimized way", or we could agree that "runnable" means "runnable once you execute a build step". Each of these ways could permit kinto-admin build outputs to not be checked in to git, because they don't belong there.
I see your point, that something we need to discuss and fix then.
Runnable means `make serve` just works. We could add a Makefile entry to fix that.

I agree that it is very annoying to have to files (eg. `git grep`)
I just found that zest.releaser has the notion of hooks:
https://github.com/zestsoftware/zest.releaser/blob/6.8.1/zest/releaser/tests/functional-with-hooks.txt

So that means we could have a release hook that builds the admin before pushing it to pypi. 

From a git clone, a `make` command would be enough.

Resolving patch:
diff --git a/kinto/plugins/admin/__init__.py b/kinto/plugins/admin/__init__.py
--- a/kinto/plugins/admin/__init__.py
+++ b/kinto/plugins/admin/__init__.py
@@ -1,15 +1,26 @@
+import json
+import os
+
 from pyramid.static import static_view
 from pyramid.httpexceptions import HTTPTemporaryRedirect
+
 from .views import admin_home_view
 
 
+HERE = os.path.dirname(__file__)
+
+
 def includeme(config):
     # Process settings to remove storage wording.
 
+    # Read version from package.json
+    package_json = json.load(open(os.path.join(HERE, "package.json")))
+    admin_version = package_json["dependencies"]["kinto-admin"]
+
     # Expose capability.
     config.add_api_capability(
         "admin",
-        version="1.10.0",
+        version=admin_version,
         description="Serves the admin console.",
         url="https://github.com/Kinto/kinto-admin/",
     )
diff --git a/kinto/plugins/admin/release_hook.py b/kinto/plugins/admin/release_hook.py
new file mode 100644
--- /dev/null
+++ b/kinto/plugins/admin/release_hook.py
@@ -0,0 +1,22 @@
+"""
+This module is a release hook for `zest.releaser <https://zestreleaser.readthedocs.io>`_
+in order to build the Kinto Admin UI bundle just before it is packaged.
+
+* See hooks in :file:`setup.cfg`
+* `Documentation <http://zestreleaser.readthedocs.io/en/latest/entrypoints.html>`_
+"""
+
+import subprocess
+
+
+def after_checkout(data):
+    """
+    During the ``release`` process, the current tag is checked out in a
+    temporary folder. We build the Kinto Admin at this step, just before
+    the files are gathered for the final Python package.
+
+    .. note::
+
+        The ``node_modules`` folder is excluded using :file:`MANIFEST.in`.
+    """
+    subprocess.run(["make", "build-kinto-admin"])
diff --git a/kinto/plugins/admin/views.py b/kinto/plugins/admin/views.py
--- a/kinto/plugins/admin/views.py
+++ b/kinto/plugins/admin/views.py
@@ -16,6 +16,11 @@ def admin_home_view(request):
     }
     globalSettings = "<script>window.globalSettings = {};</script>".format(json.dumps(settings))
 
+    try:
+        with open(os.path.join(HERE, 'build/index.html')) as f:
+            index_content = f.read()
+    except FileNotFoundError:  # pragma: no cover
+        with open(os.path.join(HERE, 'public/help.html')) as f:
+            index_content = f.read()
     # Update the file built by react-scripts to load the globalSettings.
-    with open(os.path.join(HERE, 'build/index.html')) as f:
-        return f.read().replace('<script', globalSettings + '<script')
+    return index_content.replace('<script', globalSettings + '<script')


Test patch:
diff --git a/tests/plugins/test_admin.py b/tests/plugins/test_admin.py
--- a/tests/plugins/test_admin.py
+++ b/tests/plugins/test_admin.py
@@ -1,9 +1,36 @@
+import os
 import unittest
+
+from kinto.plugins.admin import views as admin_views
 from ..support import BaseWebTest
 
 
+admin_module = os.path.abspath(os.path.dirname(admin_views.__file__))
+build_folder = os.path.join(admin_module, "build")
+built_index = os.path.join(build_folder, "index.html")
+
+
 class AdminViewTest(BaseWebTest, unittest.TestCase):
 
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        # Simulate admin bundle without npm
+        os.makedirs(build_folder, exist_ok=True)
+        with open(built_index, "w") as f:
+            f.write("<html><script/></html>")
+
+    def setUp(self):
+        admin_views.admin_home_view.saved = None
+
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+        try:
+            os.remove(built_index)
+        except FileNotFoundError:
+            pass
+
     def get_app_settings(self, extras=None):
         settings = super().get_app_settings(extras)
         settings['includes'] = 'kinto.plugins.admin'