diff --git a/passbook/admin/forms/overview.py b/passbook/admin/forms/overview.py new file mode 100644 index 000000000..727b9c145 --- /dev/null +++ b/passbook/admin/forms/overview.py @@ -0,0 +1,18 @@ +"""Forms for modals on overview page""" +from django import forms + + +class PolicyCacheClearForm(forms.Form): + """Form to clear Policy cache""" + + title = "Clear Policy cache" + body = """Are you sure you want to clear the policy cache? + This will cause all policies to be re-evaluated on their next usage.""" + + +class FlowCacheClearForm(forms.Form): + """Form to clear Flow cache""" + + title = "Clear Flow cache" + body = """Are you sure you want to clear the flow cache? + This will cause all flows to be re-evaluated on their next usage.""" diff --git a/passbook/admin/templates/administration/overview.html b/passbook/admin/templates/administration/overview.html index 95d97c031..d504096da 100644 --- a/passbook/admin/templates/administration/overview.html +++ b/passbook/admin/templates/administration/overview.html @@ -179,9 +179,12 @@
+ {% trans form.body %} +
+{% endblock %} + +{% block action %} +{% trans 'Confirm' %} +{% endblock %} diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py index 76d003672..798dbe3f7 100644 --- a/passbook/admin/urls.py +++ b/passbook/admin/urls.py @@ -26,6 +26,16 @@ from passbook.admin.views import ( urlpatterns = [ path("", shell.ShellView.as_view(), name="shell"), + path( + "overview/cache/flow/", + overview.FlowCacheClearView.as_view(), + name="overview-clear-flow-cache", + ), + path( + "overview/cache/policy/", + overview.PolicyCacheClearView.as_view(), + name="overview-clear-policy-cache", + ), path("overview/", overview.AdministrationOverviewView.as_view(), name="overview"), # Applications path( diff --git a/passbook/admin/views/overview.py b/passbook/admin/views/overview.py index 7727b2da6..e4d1ed525 100644 --- a/passbook/admin/views/overview.py +++ b/passbook/admin/views/overview.py @@ -2,14 +2,20 @@ from typing import Union from django.conf import settings +from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache from django.db.models import Count from django.db.models.fields.json import KeyTextTransform -from django.views.generic import TemplateView +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.urls import reverse_lazy +from django.utils.translation import gettext as _ +from django.views.generic import FormView, TemplateView from packaging.version import LegacyVersion, Version, parse from structlog import get_logger from passbook import __version__ +from passbook.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version from passbook.audit.models import Event, EventAction @@ -24,18 +30,6 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView): template_name = "administration/overview.html" - def post(self, *args, **kwargs): - """Handle post (clear cache from modal)""" - if "clear_policies" in self.request.POST: - keys = cache.keys("policy_*") - cache.delete_many(keys) - LOGGER.debug("Cleared Policy cache", keys=len(keys)) - if "clear_flows" in self.request.POST: - keys = cache.keys("flow_*") - cache.delete_many(keys) - LOGGER.debug("Cleared flow cache", keys=len(keys)) - return self.get(*args, **kwargs) - def get_latest_version(self) -> Union[LegacyVersion, Version]: """Get latest version from cache""" version_in_cache = cache.get(VERSION_CACHE_KEY) @@ -75,3 +69,35 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView): kwargs["cached_policies"] = len(cache.keys("policy_*")) kwargs["cached_flows"] = len(cache.keys("flow_*")) return super().get_context_data(**kwargs) + + +class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView): + """View to clear Policy cache""" + + form_class = PolicyCacheClearForm + + template_name = "generic/form_non_model.html" + success_url = reverse_lazy("passbook_admin:overview") + success_message = _("Successfully cleared Policy cache") + + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + keys = cache.keys("policy_*") + cache.delete_many(keys) + LOGGER.debug("Cleared Policy cache", keys=len(keys)) + return super().post(request, *args, **kwargs) + + +class FlowCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView): + """View to clear Flow cache""" + + form_class = FlowCacheClearForm + + template_name = "generic/form_non_model.html" + success_url = reverse_lazy("passbook_admin:overview") + success_message = _("Successfully cleared Flow cache") + + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + keys = cache.keys("flow_*") + cache.delete_many(keys) + LOGGER.debug("Cleared flow cache", keys=len(keys)) + return super().post(request, *args, **kwargs) diff --git a/passbook/static/static/dist/main.js.map b/passbook/static/static/dist/main.js.map index 28d1f1102..57ad4ae46 100644 --- a/passbook/static/static/dist/main.js.map +++ b/passbook/static/static/dist/main.js.map @@ -1 +1 @@ -{"version":3,"file":"main.js","sources":["../src/legacy.js","../node_modules/tslib/tslib.es6.js","../node_modules/lit-html/src/lib/dom.ts","../node_modules/lit-html/src/lib/template.ts","../node_modules/lit-html/src/lib/modify-template.ts","../node_modules/lit-html/src/lib/directive.ts","../node_modules/lit-html/src/lib/part.ts","../node_modules/lit-html/src/lib/template-instance.ts","../node_modules/lit-html/src/lib/template-result.ts","../node_modules/lit-html/src/lib/parts.ts","../node_modules/lit-html/src/lib/template-factory.ts","../node_modules/lit-html/src/lib/render.ts","../node_modules/lit-html/src/lib/default-template-processor.ts","../node_modules/lit-html/src/lit-html.ts","../node_modules/lit-html/src/lib/shady-render.ts","../node_modules/lit-element/src/lib/updating-element.ts","../node_modules/lit-element/src/lib/decorators.ts","../node_modules/lit-element/src/lib/css-tag.ts","../node_modules/lit-element/src/lit-element.ts","../src/Messages.ts","../src/ActionButton.ts","../src/utils.ts","../src/AdminSidebar.ts","../src/AdminSiteShell.ts","../node_modules/moment/moment.js","../node_modules/chart.js/dist/Chart.js","../src/AdminLoginsChart.ts","../node_modules/codemirror/lib/codemirror.js","../node_modules/codemirror/addon/display/autorefresh.js","../node_modules/codemirror/mode/xml/xml.js","../node_modules/codemirror/mode/yaml/yaml.js","../node_modules/codemirror/mode/python/python.js","../src/CodeMirror.ts","../src/Dropdown.ts","../src/FetchFillSlot.ts","../src/FlowShellCard.ts","../src/ModalButton.ts","../src/Tabs.ts"],"sourcesContent":["// Search clearing\ndocument.querySelectorAll(\"input[type=search]\").forEach((si) => {\n si.addEventListener(\"search\", (e) => {\n if (si.value === \"\") {\n si.parentElement.submit();\n }\n });\n});\n\n// Fetch from data-attributes\ndocument.querySelectorAll(\"[data-pb-fetch-fill]\").forEach((el) => {\n const url = el.dataset.pbFetchFill;\n const key = el.dataset.pbFetchKey;\n fetch(url).then(r => r.json()).then(r => {\n el.textContent = r[key];\n el.value = r[key];\n });\n});\n\n// Modal\ndocument.querySelectorAll(\"[data-target='modal']\").forEach((m) => {\n m.addEventListener(\"click\", (e) => {\n const parentContainer = e.target.closest('[data-target=\"modal\"]');\n const modalId = parentContainer.attributes['data-modal'].value;\n document.querySelector(`#${modalId}`).removeAttribute(\"hidden\");\n });\n});\ndocument.querySelectorAll(\".pf-c-modal-box [data-modal-close]\").forEach((b) => {\n b.addEventListener(\"click\", (e) => {\n const parentContainer = e.target.closest('.pf-c-backdrop');\n parentContainer.setAttribute(\"hidden\", true);\n });\n});\n\n// Make Checkbox label click trigger checkbox toggle\ndocument.querySelectorAll(\".pf-c-check__label\").forEach((checkLabel) => {\n checkLabel.addEventListener(\"click\", (e) => {\n const checkbox = e.target.parentElement.querySelector(\"input[type=checkbox]\");\n checkbox.checked = !checkbox.checked;\n });\n});\n\n// CodeMirror\n// document.querySelectorAll(\".codemirror\").forEach((cm) => {\n// let cmMode = 'xml';\n// if ('data-cm-mode' in cm.attributes) {\n// cmMode = cm.attributes['data-cm-mode'].value;\n// }\n// // https://github.com/codemirror/CodeMirror/issues/5092\n// cm.removeAttribute(\"required\");\n// CodeMirror.fromTextArea(cm, {\n// mode: cmMode,\n// theme: 'monokai',\n// lineNumbers: false,\n// readOnly: cm.readOnly,\n// autoRefresh: true,\n// });\n// });\n\n// Automatic slug fields\nconst convertToSlug = (text) => {\n return text\n .toLowerCase()\n .replace(/ /g, '-')\n .replace(/[^\\w-]+/g, '');\n};\n\ndocument.querySelectorAll(\"input[name=name]\").forEach((input) => {\n input.addEventListener(\"input\", (e) => {\n const form = e.target.closest(\"form\");\n if (form === null) {\n return;\n }\n const slugField = form.querySelector(\"input[name=slug]\");\n slugField.value = convertToSlug(e.target.value);\n });\n});\n\n// Hamburger Menu\ndocument.querySelectorAll(\".pf-c-page__header-brand-toggle>button\").forEach((toggle) => {\n toggle.addEventListener(\"click\", (e) => {\n const sidebar = document.querySelector(\".pf-c-page__sidebar\");\n if (sidebar.classList.contains(\"pf-m-expanded\")) {\n // Sidebar already expanded\n sidebar.classList.remove(\"pf-m-expanded\");\n sidebar.style.zIndex = 0;\n } else {\n // Sidebar not expanded yet\n sidebar.classList.add(\"pf-m-expanded\");\n sidebar.style.zIndex = 200;\n }\n });\n});\n\n// Collapsable Menus in Sidebar\ndocument.querySelectorAll(\".pf-m-expandable>.pf-c-nav__link\").forEach((menu) => {\n menu.addEventListener(\"click\", (e) => {\n e.preventDefault();\n menu.parentElement.classList.toggle(\"pf-m-expanded\");\n });\n});\n","/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n};\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, privateMap) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to get private field on non-instance\");\r\n }\r\n return privateMap.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, privateMap, value) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to set private field on non-instance\");\r\n }\r\n privateMap.set(receiver, value);\r\n return value;\r\n}\r\n","/**\n * @license\n * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.\n * This code may only be used under the BSD style license found at\n * http://polymer.github.io/LICENSE.txt\n * The complete set of authors may be found at\n * http://polymer.github.io/AUTHORS.txt\n * The complete set of contributors may be found at\n * http://polymer.github.io/CONTRIBUTORS.txt\n * Code distributed by Google as part of the polymer project is also\n * subject to an additional IP rights grant found at\n * http://polymer.github.io/PATENTS.txt\n */\n\ninterface MaybePolyfilledCe extends CustomElementRegistry {\n readonly polyfillWrapFlushCallback?: object;\n}\n\n/**\n * True if the custom elements polyfill is in use.\n */\nexport const isCEPolyfill = typeof window !== 'undefined' &&\n window.customElements != null &&\n (window.customElements as MaybePolyfilledCe).polyfillWrapFlushCallback !==\n undefined;\n\n/**\n * Reparents nodes, starting from `start` (inclusive) to `end` (exclusive),\n * into another container (could be the same container), before `before`. If\n * `before` is null, it appends the nodes to the container.\n */\nexport const reparentNodes =\n (container: Node,\n start: Node|null,\n end: Node|null = null,\n before: Node|null = null): void => {\n while (start !== end) {\n const n = start!.nextSibling;\n container.insertBefore(start!, before);\n start = n;\n }\n };\n\n/**\n * Removes nodes, starting from `start` (inclusive) to `end` (exclusive), from\n * `container`.\n */\nexport const removeNodes =\n (container: Node, start: Node|null, end: Node|null = null): void => {\n while (start !== end) {\n const n = start!.nextSibling;\n container.removeChild(start!);\n start = n;\n }\n };\n","/**\n * @license\n * Copyright (c) 2017 The Polymer Project Authors. All rights reserved.\n * This code may only be used under the BSD style license found at\n * http://polymer.github.io/LICENSE.txt\n * The complete set of authors may be found at\n * http://polymer.github.io/AUTHORS.txt\n * The complete set of contributors may be found at\n * http://polymer.github.io/CONTRIBUTORS.txt\n * Code distributed by Google as part of the polymer project is also\n * subject to an additional IP rights grant found at\n * http://polymer.github.io/PATENTS.txt\n */\n\nimport {TemplateResult} from './template-result.js';\n\n/**\n * An expression marker with embedded unique key to avoid collision with\n * possible text in templates.\n */\nexport const marker = `{{lit-${String(Math.random()).slice(2)}}}`;\n\n/**\n * An expression marker used text-positions, multi-binding attributes, and\n * attributes with markup-like text values.\n */\nexport const nodeMarker = ``;\n\nexport const markerRegex = new RegExp(`${marker}|${nodeMarker}`);\n\n/**\n * Suffix appended to all bound attribute names.\n */\nexport const boundAttributeSuffix = '$lit$';\n\n/**\n * An updatable Template that tracks the location of dynamic parts.\n */\nexport class Template {\n readonly parts: TemplatePart[] = [];\n readonly element: HTMLTemplateElement;\n\n constructor(result: TemplateResult, element: HTMLTemplateElement) {\n this.element = element;\n\n const nodesToRemove: Node[] = [];\n const stack: Node[] = [];\n // Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null\n const walker = document.createTreeWalker(\n element.content,\n 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */,\n null,\n false);\n // Keeps track of the last index associated with a part. We try to delete\n // unnecessary nodes, but we never want to associate two different parts\n // to the same index. They must have a constant node between.\n let lastPartIndex = 0;\n let index = -1;\n let partIndex = 0;\n const {strings, values: {length}} = result;\n while (partIndex < length) {\n const node = walker.nextNode() as Element | Comment | Text | null;\n if (node === null) {\n // We've exhausted the content inside a nested template element.\n // Because we still have parts (the outer for-loop), we know:\n // - There is a template in the stack\n // - The walker will find a nextNode outside the template\n walker.currentNode = stack.pop()!;\n continue;\n }\n index++;\n\n if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {\n if ((node as Element).hasAttributes()) {\n const attributes = (node as Element).attributes;\n const {length} = attributes;\n // Per\n // https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap,\n // attributes are not guaranteed to be returned in document order.\n // In particular, Edge/IE can return them out of order, so we cannot\n // assume a correspondence between part index and attribute index.\n let count = 0;\n for (let i = 0; i < length; i++) {\n if (endsWith(attributes[i].name, boundAttributeSuffix)) {\n count++;\n }\n }\n while (count-- > 0) {\n // Get the template literal section leading up to the first\n // expression in this attribute\n const stringForPart = strings[partIndex];\n // Find the attribute name\n const name = lastAttributeNameRegex.exec(stringForPart)![2];\n // Find the corresponding attribute\n // All bound attributes have had a suffix added in\n // TemplateResult#getHTML to opt out of special attribute\n // handling. To look up the attribute value we also need to add\n // the suffix.\n const attributeLookupName =\n name.toLowerCase() + boundAttributeSuffix;\n const attributeValue =\n (node as Element).getAttribute(attributeLookupName)!;\n (node as Element).removeAttribute(attributeLookupName);\n const statics = attributeValue.split(markerRegex);\n this.parts.push({type: 'attribute', index, name, strings: statics});\n partIndex += statics.length - 1;\n }\n }\n if ((node as Element).tagName === 'TEMPLATE') {\n stack.push(node);\n walker.currentNode = (node as HTMLTemplateElement).content;\n }\n } else if (node.nodeType === 3 /* Node.TEXT_NODE */) {\n const data = (node as Text).data;\n if (data.indexOf(marker) >= 0) {\n const parent = node.parentNode!;\n const strings = data.split(markerRegex);\n const lastIndex = strings.length - 1;\n // Generate a new text node for each literal section\n // These nodes are also used as the markers for node parts\n for (let i = 0; i < lastIndex; i++) {\n let insert: Node;\n let s = strings[i];\n if (s === '') {\n insert = createMarker();\n } else {\n const match = lastAttributeNameRegex.exec(s);\n if (match !== null && endsWith(match[2], boundAttributeSuffix)) {\n s = s.slice(0, match.index) + match[1] +\n match[2].slice(0, -boundAttributeSuffix.length) + match[3];\n }\n insert = document.createTextNode(s);\n }\n parent.insertBefore(insert, node);\n this.parts.push({type: 'node', index: ++index});\n }\n // If there's no text, we must insert a comment to mark our place.\n // Else, we can trust it will stick around after cloning.\n if (strings[lastIndex] === '') {\n parent.insertBefore(createMarker(), node);\n nodesToRemove.push(node);\n } else {\n (node as Text).data = strings[lastIndex];\n }\n // We have a part for each match found\n partIndex += lastIndex;\n }\n } else if (node.nodeType === 8 /* Node.COMMENT_NODE */) {\n if ((node as Comment).data === marker) {\n const parent = node.parentNode!;\n // Add a new marker node to be the startNode of the Part if any of\n // the following are true:\n // * We don't have a previousSibling\n // * The previousSibling is already the start of a previous part\n if (node.previousSibling === null || index === lastPartIndex) {\n index++;\n parent.insertBefore(createMarker(), node);\n }\n lastPartIndex = index;\n this.parts.push({type: 'node', index});\n // If we don't have a nextSibling, keep this node so we have an end.\n // Else, we can remove it to save future costs.\n if (node.nextSibling === null) {\n (node as Comment).data = '';\n } else {\n nodesToRemove.push(node);\n index--;\n }\n partIndex++;\n } else {\n let i = -1;\n while ((i = (node as Comment).data.indexOf(marker, i + 1)) !== -1) {\n // Comment node has a binding marker inside, make an inactive part\n // The binding won't work, but subsequent bindings will\n // TODO (justinfagnani): consider whether it's even worth it to\n // make bindings in comments work\n this.parts.push({type: 'node', index: -1});\n partIndex++;\n }\n }\n }\n }\n\n // Remove text binding nodes after the walk to not disturb the TreeWalker\n for (const n of nodesToRemove) {\n n.parentNode!.removeChild(n);\n }\n }\n}\n\nconst endsWith = (str: string, suffix: string): boolean => {\n const index = str.length - suffix.length;\n return index >= 0 && str.slice(index) === suffix;\n};\n\n/**\n * A placeholder for a dynamic expression in an HTML template.\n *\n * There are two built-in part types: AttributePart and NodePart. NodeParts\n * always represent a single dynamic expression, while AttributeParts may\n * represent as many expressions are contained in the attribute.\n *\n * A Template's parts are mutable, so parts can be replaced or modified\n * (possibly to implement different template semantics). The contract is that\n * parts can only be replaced, not removed, added or reordered, and parts must\n * always consume the correct number of values in their `update()` method.\n *\n * TODO(justinfagnani): That requirement is a little fragile. A\n * TemplateInstance could instead be more careful about which values it gives\n * to Part.update().\n */\nexport type TemplatePart = {\n readonly type: 'node'; index: number;\n}|{\n readonly type: 'attribute';\n index: number;\n readonly name: string;\n readonly strings: ReadonlyArray