diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f2e2c8..5467b73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## CHANGELOG +### [25.2.0] - Jan 6, 2025 +- Added a "Delete Link" button to allow DataTemplates to be deleted +- Added support so a specific DataSet can be selected via the DataTemplate URL using `?ds=` or `/dt/
/` +- Don't add a hash symbol to the end of the URL when clicking on links +- Fixed an issue where the "Protect Link" button was losing it's icon under certain conditions + ### [25.1.1] - Jan 1, 2025 - Rewritten Web Log authentication so it will now prompt you for a password @@ -368,6 +374,7 @@ - Initial release +[25.2.0]: https://github.com/cmason3/jinjafx_server/compare/25.1.1...25.2.0 [25.1.1]: https://github.com/cmason3/jinjafx_server/compare/25.1.0...25.1.1 [25.1.0]: https://github.com/cmason3/jinjafx_server/compare/24.12.1...25.1.0 [24.12.1]: https://github.com/cmason3/jinjafx_server/compare/24.12.0...24.12.1 diff --git a/README.md b/README.md index 508bee7..1c6f730 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ As well as supporting the standard CodeMirror shortcut keys for the "data.csv", The DataSet feature allows you to include multiple different "data.csv" and "vars.yml" contents while maintaining the same "template.j2". When enabled it also enables a "global.yml" pane for specifying global variables which are common across all DataSets (any variables which are redefined in "vars.yml" will overwrite variables in "global.yml"). This is to support scenarios where you have different DataSets for your Live vs your Test environments, but the template should be the same. There are no limits on the number of different DataSets that can be added to a single DataTemplate (the name must start with a letter and only contain alphanumerical, "-", " " or "_" characters). When you click "Generate" it will use the currently active DataSet to generate the output - clicking on the name of the current DataSet (by default there is a single "Default" DataSet) allows you to switch between the different DataSets. +You can also select a specific DataSet when you load a DataTemplate using the `ds` query string parameter (i.e. `?ds='`) or by adding an additional path element (i.e. `/dt/
/`). + ### Output Formats JinjaFx Server supports the ability to use "output" tags to create different outputs with different names like JinjaFx, but it also allows you to optionally specify how you want the output to be rendered. By default, the output is rendered as "text" but you also have the option to specify "html" and "markdown" (for GitHub Flavoured Markdown), which will result in the output being rendered appropriately, e.g: diff --git a/jinjafx_server.py b/jinjafx_server.py index 71e8d18..9417ad5 100755 --- a/jinjafx_server.py +++ b/jinjafx_server.py @@ -26,7 +26,7 @@ import re, argparse, hashlib, traceback, glob, hmac, uuid, struct, binascii, gzip, requests, ctypes, subprocess import cmarkgfm, emoji -__version__ = '25.1.1' +__version__ = '25.2.0' llock = threading.RLock() rlock = threading.RLock() @@ -247,7 +247,7 @@ def do_GET(self, head=False, cache=True, versioned=False): fpath = '/index.html' self.hide = not verbose - if re.search(r'^/dt/[A-Za-z0-9_-]{1,24}$', fpath): + if re.search(r'^/dt/[A-Za-z0-9_-]{1,24}(?:/[A-Za-z][A-Za-z0-9_ %-]*)?$', fpath): fpath = '/index.html' if re.search(r'^/[a-f0-9]{8}/', fpath): @@ -621,10 +621,11 @@ def html_escape(text): else: r = [ 'text/plain', 400, '400 Bad Request\r\n', sys._getframe().f_lineno ] - elif fpath == '/get_link': + elif fpath == '/get_link' or fpath == '/delete_link': if aws_s3_url or github_url or repository: if self.headers['Content-Type'] == 'application/json': try: + dt_yml = '' dt_password = '' dt_opassword = '' dt_mpassword = '' @@ -650,88 +651,132 @@ def html_escape(text): dt_revision = int(self.headers['X-Dt-Revision']) if not self.ratelimit(remote_addr, 1, False): - dt = json.loads(postdata.decode('utf-8')) - - vdt = {} - - dt_yml = '---\n' - dt_yml += 'dt:\n' + def authenticate_dt(rdt, r): + mm = re.search(r'dt_mpassword: "(\S+)"', rdt) + mo = re.search(r'dt_password: "(\S+)"', rdt) - if 'datasets' in dt: - if 'global' in dt: - vdt['global'] = self.d(dt['global']).decode('utf-8') if 'global' in dt and len(dt['global'].strip()) > 0 else '' + if mm != None or mo != None: + if dt_password != '': + rpassword = mm.group(1) if mm != None else mo.group(1) + t = binascii.unhexlify(rpassword.encode('utf-8')) + if t != self.derive_key(dt_password, t[2:int(t[1]) + 2], t[0]): + cheaders['X-Dt-Authentication'] = 'Modify' if (mm != None) else 'Open' + r = [ 'text/plain', 401, '401 Unauthorized\r\n', sys._getframe().f_lineno ] - if vdt['global'] == '': - dt_yml += ' global: ""\n\n' else: - dt_yml += ' global: |2\n' - dt_yml += re.sub('^', ' ' * 4, vdt['global'].rstrip(), flags=re.MULTILINE) + '\n\n' - - dt_yml += ' datasets:\n' + cheaders['X-Dt-Authentication'] = 'Modify' if (mm != None) else 'Open' + r = [ 'text/plain', 401, '401 Unauthorized\r\n', sys._getframe().f_lineno ] - for ds in dt['datasets']: - vdt['data'] = self.d(dt['datasets'][ds]['data']).decode('utf-8') if 'data' in dt['datasets'][ds] and len(dt['datasets'][ds]['data'].strip()) > 0 else '' - vdt['vars'] = self.d(dt['datasets'][ds]['vars']).decode('utf-8') if 'vars' in dt['datasets'][ds] and len(dt['datasets'][ds]['vars'].strip()) > 0 else '' + return r - dt_yml += ' "' + ds + '":\n' + if fpath == '/get_link': + dt = json.loads(postdata.decode('utf-8')) + vdt = {} + dt_yml += '---\n' + dt_yml += 'dt:\n' + + if 'datasets' in dt: + if 'global' in dt: + vdt['global'] = self.d(dt['global']).decode('utf-8') if 'global' in dt and len(dt['global'].strip()) > 0 else '' + + if vdt['global'] == '': + dt_yml += ' global: ""\n\n' + else: + dt_yml += ' global: |2\n' + dt_yml += re.sub('^', ' ' * 4, vdt['global'].rstrip(), flags=re.MULTILINE) + '\n\n' + + dt_yml += ' datasets:\n' + + for ds in dt['datasets']: + vdt['data'] = self.d(dt['datasets'][ds]['data']).decode('utf-8') if 'data' in dt['datasets'][ds] and len(dt['datasets'][ds]['data'].strip()) > 0 else '' + vdt['vars'] = self.d(dt['datasets'][ds]['vars']).decode('utf-8') if 'vars' in dt['datasets'][ds] and len(dt['datasets'][ds]['vars'].strip()) > 0 else '' + + dt_yml += ' "' + ds + '":\n' + + if vdt['data'] == '': + dt_yml += ' data: ""\n' + else: + dt_yml += ' data: |2\n' + dt_yml += re.sub('^', ' ' * 8, vdt['data'].rstrip(), flags=re.MULTILINE) + '\n\n' + + if vdt['vars'] == '': + dt_yml += ' vars: ""\n\n' + else: + dt_yml += ' vars: |2\n' + dt_yml += re.sub('^', ' ' * 8, vdt['vars'].rstrip(), flags=re.MULTILINE) + '\n\n' + + else : + vdt['data'] = self.d(dt['data']).decode('utf-8') if 'data' in dt and len(dt['data'].strip()) > 0 else '' + vdt['vars'] = self.d(dt['vars']).decode('utf-8') if 'vars' in dt and len(dt['vars'].strip()) > 0 else '' + if vdt['data'] == '': - dt_yml += ' data: ""\n' + dt_yml += ' data: ""\n' else: - dt_yml += ' data: |2\n' - dt_yml += re.sub('^', ' ' * 8, vdt['data'].rstrip(), flags=re.MULTILINE) + '\n\n' - + dt_yml += ' data: |2\n' + dt_yml += re.sub('^', ' ' * 4, vdt['data'].rstrip(), flags=re.MULTILINE) + '\n\n' + if vdt['vars'] == '': - dt_yml += ' vars: ""\n\n' + dt_yml += ' vars: ""\n\n' else: - dt_yml += ' vars: |2\n' - dt_yml += re.sub('^', ' ' * 8, vdt['vars'].rstrip(), flags=re.MULTILINE) + '\n\n' - - else : - vdt['data'] = self.d(dt['data']).decode('utf-8') if 'data' in dt and len(dt['data'].strip()) > 0 else '' - vdt['vars'] = self.d(dt['vars']).decode('utf-8') if 'vars' in dt and len(dt['vars'].strip()) > 0 else '' - - if vdt['data'] == '': - dt_yml += ' data: ""\n' - else: - dt_yml += ' data: |2\n' - dt_yml += re.sub('^', ' ' * 4, vdt['data'].rstrip(), flags=re.MULTILINE) + '\n\n' - - if vdt['vars'] == '': - dt_yml += ' vars: ""\n\n' + dt_yml += ' vars: |2\n' + dt_yml += re.sub('^', ' ' * 4, vdt['vars'].rstrip(), flags=re.MULTILINE) + '\n\n' + + if isinstance(dt['template'], dict): + dt_yml += ' template:\n' + + for t in dt['template']: + te = self.d(dt['template'][t]).decode('utf-8') if len(dt['template'][t].strip()) > 0 else '' + + if te == '': + dt_yml += ' "' + t + '": ""\n' + else: + dt_yml += ' "' + t + '": |2\n' + dt_yml += re.sub('^', ' ' * 6, te, flags=re.MULTILINE) + '\n\n' + else: - dt_yml += ' vars: |2\n' - dt_yml += re.sub('^', ' ' * 4, vdt['vars'].rstrip(), flags=re.MULTILINE) + '\n\n' - - if isinstance(dt['template'], dict): - dt_yml += ' template:\n' - - for t in dt['template']: - te = self.d(dt['template'][t]).decode('utf-8') if len(dt['template'][t].strip()) > 0 else '' - + te = self.d(dt['template']).decode('utf-8') if len(dt['template'].strip()) > 0 else '' + if te == '': - dt_yml += ' "' + t + '": ""\n' + dt_yml += ' template: ""\n' else: - dt_yml += ' "' + t + '": |2\n' - dt_yml += re.sub('^', ' ' * 6, te, flags=re.MULTILINE) + '\n\n' - - else: - te = self.d(dt['template']).decode('utf-8') if len(dt['template'].strip()) > 0 else '' - - if te == '': - dt_yml += ' template: ""\n' - else: - dt_yml += ' template: |2\n' - dt_yml += re.sub('^', ' ' * 4, te, flags=re.MULTILINE) + '\n\n' - - if not dt_yml.endswith('\n\n'): - dt_yml += '\n' - - dt_yml += 'revision: ' + str(dt_revision) + '\n' - dt_yml += 'dataset: "' + dt['dataset'] + '"\n' - - if dt_encrypted: - dt_yml += 'encrypted: 1\n' + dt_yml += ' template: |2\n' + dt_yml += re.sub('^', ' ' * 4, te, flags=re.MULTILINE) + '\n\n' + + if not dt_yml.endswith('\n\n'): + dt_yml += '\n' + + dt_yml += 'revision: ' + str(dt_revision) + '\n' + dt_yml += 'dataset: "' + dt['dataset'] + '"\n' + + if dt_encrypted: + dt_yml += 'encrypted: 1\n' + + def update_dt(rdt, dt_yml, r): + r = authenticate_dt(rdt, r) + + if r[1] != 401: + if dt_protected: + if dt_opassword != '' or dt_mpassword != '': + if dt_opassword != '': + dt_yml += 'dt_password: "' + binascii.hexlify(self.derive_key(dt_opassword)).decode('utf-8') + '"\n' + + if dt_mpassword != '': + dt_yml += 'dt_mpassword: "' + binascii.hexlify(self.derive_key(dt_mpassword)).decode('utf-8') + '"\n' + + else: + if mo != None: + dt_yml += 'dt_password: "' + mo.group(1) + '"\n' + + if mm != None: + dt_yml += 'dt_mpassword: "' + mm.group(1) + '"\n' + + return dt_yml, r + + def add_client_fields(dt_yml, remote_addr): + dt_yml += 'remote_addr: "' + remote_addr + '"\n' + dt_yml += 'updated: "' + str(int(time.time())) + '"\n' + return dt_yml if 'id' in params: if re.search(r'^[A-Za-z0-9_-]{1,24}$', params['id']): @@ -740,50 +785,14 @@ def html_escape(text): else: raise Exception("invalid link format") + elif fpath == '/delete_link': + raise Exception("link id is required") + else: dt_link = self.encode_link(hashlib.sha256((str(uuid.uuid1()) + ':' + dt_yml).encode('utf-8')).digest()[:6]) dt_filename = 'jfx_' + dt_link + '.yml' - def update_dt(rdt, dt_yml, r): - mm = re.search(r'dt_mpassword: "(\S+)"', rdt) - mo = re.search(r'dt_password: "(\S+)"', rdt) - - if mm != None or mo != None: - if dt_password != '': - rpassword = mm.group(1) if mm != None else mo.group(1) - t = binascii.unhexlify(rpassword.encode('utf-8')) - if t != self.derive_key(dt_password, t[2:int(t[1]) + 2], t[0]): - cheaders['X-Dt-Authentication'] = 'Modify' if (mm != None) else 'Open' - r = [ 'text/plain', 401, '401 Unauthorized\r\n', sys._getframe().f_lineno ] - - else: - cheaders['X-Dt-Authentication'] = 'Modify' if (mm != None) else 'Open' - r = [ 'text/plain', 401, '401 Unauthorized\r\n', sys._getframe().f_lineno ] - - if r[1] != 401: - if dt_protected: - if dt_opassword != '' or dt_mpassword != '': - if dt_opassword != '': - dt_yml += 'dt_password: "' + binascii.hexlify(self.derive_key(dt_opassword)).decode('utf-8') + '"\n' - - if dt_mpassword != '': - dt_yml += 'dt_mpassword: "' + binascii.hexlify(self.derive_key(dt_mpassword)).decode('utf-8') + '"\n' - - else: - if mo != None: - dt_yml += 'dt_password: "' + mo.group(1) + '"\n' - - if mm != None: - dt_yml += 'dt_mpassword: "' + mm.group(1) + '"\n' - - return dt_yml, r - - def add_client_fields(dt_yml, remote_addr): - dt_yml += 'remote_addr: "' + remote_addr + '"\n' - dt_yml += 'updated: "' + str(int(time.time())) + '"\n' - return dt_yml - if aws_s3_url: rr = aws_s3_get(aws_s3_url, dt_filename) if rr.status_code == 200: @@ -808,9 +817,22 @@ def add_client_fields(dt_yml, remote_addr): r = [ 'text/plain', 409, '409 Conflict\r\n', sys._getframe().f_lineno ] if r[1] != 409: - dt_yml, r = update_dt(rr.text, dt_yml, r) + if fpath == '/get_link': + dt_yml, r = update_dt(rr.text, dt_yml, r) + + elif fpath == '/delete_link': + r = authenticate_dt(rr.text, r) + + if r[1] != 401: + rr = aws_s3_delete(aws_s3_url, dt_filename) - if r[1] == 500 or r[1] == 200: + if rr.status_code == 204: + r = [ 'text/plain', 200, '200 OK\r\n', sys._getframe().f_lineno ] + + elif rr.status_code == 403: + r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] + + if fpath != '/delete_link' and (r[1] == 500 or r[1] == 200): dt_yml = add_client_fields(dt_yml, remote_addr) if dt_encrypted: @@ -866,9 +888,22 @@ def add_client_fields(dt_yml, remote_addr): r = [ 'text/plain', 409, '409 Conflict\r\n', sys._getframe().f_lineno ] if r[1] != 409: - dt_yml, r = update_dt(content, dt_yml, r) + if fpath == '/get_link': + dt_yml, r = update_dt(content, dt_yml, r) + + elif fpath == '/delete_link': + r = authenticate_dt(content, r) + + if r[1] != 401: + rr = github_delete(github_url, dt_filename, sha) - if r[1] == 500 or r[1] == 200: + if str(rr.status_code).startswith('2'): + r = [ 'text/plain', 200, '200 OK\r\n', sys._getframe().f_lineno ] + + elif rr.status_code == 401: + r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] + + if fpath != '/delete_link' and (r[1] == 500 or r[1] == 200): dt_yml = add_client_fields(dt_yml, remote_addr) if dt_encrypted: @@ -890,9 +925,6 @@ def add_client_fields(dt_yml, remote_addr): elif rr.status_code == 401: r = [ 'text/plain', 403, '403 Forbidden\r\n', sys._getframe().f_lineno ] - else: - print(rr.text) - else: dt_filename = os.path.normpath(repository + '/' + dt_filename) @@ -918,9 +950,17 @@ def add_client_fields(dt_yml, remote_addr): r = [ 'text/plain', 409, '409 Conflict\r\n', sys._getframe().f_lineno ] if r[1] != 409: - dt_yml, r = update_dt(rr, dt_yml, r) + if fpath == '/get_link': + dt_yml, r = update_dt(rr, dt_yml, r) + + elif fpath == '/delete_link': + r = authenticate_dt(rr, r) - if r[1] == 500 or r[1] == 200: + if r[1] != 401: + os.remove(dt_filename) + r = [ 'text/plain', 200, '200 OK\r\n', sys._getframe().f_lineno ] + + if fpath != '/delete_link' and (r[1] == 500 or r[1] == 200): dt_yml = add_client_fields(dt_yml, remote_addr) if dt_encrypted: @@ -933,11 +973,11 @@ def add_client_fields(dt_yml, remote_addr): else: r = [ 'text/plain', 400, '400 Bad Request\r\n', sys._getframe().f_lineno ] - if r[1] == 500 or r[1] == 200: - with open(dt_filename, 'w') as f: - f.write(dt_yml) + if r[1] != 400: + with open(dt_filename, 'w') as f: + f.write(dt_yml) - r = [ 'text/plain', 200, dt_link + '\r\n', sys._getframe().f_lineno ] + r = [ 'text/plain', 200, dt_link + '\r\n', sys._getframe().f_lineno ] else: r = [ 'text/plain', 429, '429 Too Many Requests\r\n', sys._getframe().f_lineno ] @@ -1229,6 +1269,17 @@ def aws_s3_authorization(method, fname, region, headers): return headers +def aws_s3_delete(s3_url, fname): + headers = { + 'Host': s3_url, + 'Content-Type': 'text/plain', + 'x-amz-content-sha256': hashlib.sha256(b'').hexdigest(), + 'x-amz-date': datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ') + } + headers = aws_s3_authorization('DELETE', fname, s3_url.split('.')[2], headers) + return requests.delete('https://' + s3_url + '/' + fname, headers=headers) + + def aws_s3_put(s3_url, fname, content, ctype): content = gzip.compress(content.encode('utf-8')) headers = { @@ -1254,6 +1305,25 @@ def aws_s3_get(s3_url, fname): return requests.get('https://' + s3_url + '/' + fname, headers=headers) +def github_delete(github_url, fname, sha=None): + headers = { + 'Authorization': 'Token ' + github_token, + 'Content-Type': 'application/json' + } + + data = { + 'message': 'Delete ' + fname + } + + if ':' in github_url: + github_url, data['branch'] = github_url.split(':', 1) + + if sha is not None: + data['sha'] = sha + + return requests.delete('https://api.github.com/repos/' + github_url + '/contents/' + fname, headers=headers, data=json.dumps(data)) + + def github_put(github_url, fname, content, sha=None): headers = { 'Authorization': 'Token ' + github_token, diff --git a/www/index.html b/www/index.html index fe4cd1b..e26c250 100644 --- a/www/index.html +++ b/www/index.html @@ -32,7 +32,7 @@ - +
@@ -100,17 +100,25 @@
diff --git a/www/jinjafx_logs.js b/www/jinjafx_logs.js index 2aa5216..8e37f21 100644 --- a/www/jinjafx_logs.js +++ b/www/jinjafx_logs.js @@ -70,7 +70,7 @@ if (qs.has('key')) { key = qs.get('key'); - window.history.replaceState(null, null, window.location.pathname); + window.history.replaceState({}, document.title, window.location.pathname); sessionStorage.removeItem('jfx_weblog_key'); update(); } diff --git a/www/jinjafx_m.js b/www/jinjafx_m.js index 631c7fb..5d0112e 100644 --- a/www/jinjafx_m.js +++ b/www/jinjafx_m.js @@ -40,7 +40,6 @@ function _utob(c) { } function utob(u) { - // Borrowed from Dan Kogai (https://github.com/dankogai/js-base64) return u.replace(/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g, _utob); } @@ -57,7 +56,6 @@ function _btou(cccc) { } function btou(b) { - // Borrowed from Dan Kogai (https://github.com/dankogai/js-base64) return b.replace(/[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g, _btou); } @@ -119,6 +117,7 @@ function getStatusText(code) { var dt_opassword = null; var dt_mpassword = null; var dt_epassword = null; + var delete_pending = false; var input_form = null; var r_input_form = null; var jinput = null; @@ -201,7 +200,7 @@ function getStatusText(code) { var a = document.createElement('a'); a.classList.add('dropdown-item', 'text-decoration-none'); a.addEventListener('click', select_dataset, false); - a.href = '#'; + a.href = 'javascript:void(0)'; a.ds_name = ds; a.innerHTML = ds; document.getElementById('datasets').appendChild(a); @@ -254,7 +253,7 @@ function getStatusText(code) { var a = document.createElement('a'); a.classList.add('dropdown-item', 'text-decoration-none'); a.addEventListener('click', select_template, false); - a.href = '#'; + a.href = 'javascript:void(0)'; a.t_name = t; a.innerHTML = t; document.getElementById('templates').appendChild(a); @@ -418,6 +417,14 @@ function getStatusText(code) { return false; } + if (method == "delete") { + if (confirm("Are You Sure?") === true) { + set_wait(); + update_link(dt_id, true); + } + return false; + } + switch_template(current_t, true, false); if (templates['Default'].getValue().length === 0) { window.cmTemplate.focus(); @@ -698,10 +705,10 @@ function getStatusText(code) { set_wait(); if (method == "update_link") { - update_link(dt_id); + update_link(dt_id, false); } else { - update_link(null); + update_link(null, false); } } } @@ -713,11 +720,19 @@ function getStatusText(code) { } } - function update_link(v_dt_id) { + function update_link(v_dt_id, dflag) { var xHR = new XMLHttpRequest(); if (v_dt_id !== null) { - xHR.open("POST", "/get_link?id=" + v_dt_id, true); + if (dflag) { + if (!delete_pending) { + dt_password = null; + } + xHR.open("POST", "/delete_link?id=" + v_dt_id, true); + } + else { + xHR.open("POST", "/get_link?id=" + v_dt_id, true); + } xHR.setRequestHeader("X-Dt-Protected", dt_protected ? 1 : 0); if (dt_password !== null) { xHR.setRequestHeader("X-Dt-Password", dt_password); @@ -741,26 +756,38 @@ function getStatusText(code) { xHR.onload = function() { if (this.status === 200) { if (v_dt_id !== null) { - revision += 1; - if (dt_protected) { - if (dt_mpassword != null) { - dt_password = dt_mpassword; - } - else if (dt_opassword != null) { - dt_password = dt_opassword; + if (!dflag) { + revision += 1; + if (dt_protected) { + if (dt_mpassword != null) { + dt_password = dt_mpassword; + } + else if (dt_opassword != null) { + dt_password = dt_opassword; + } + if (dt_opassword != null) { + dt_epassword = dt_opassword; + } } - if (dt_opassword != null) { - dt_epassword = dt_opassword; + else { + dt_epassword = null; + dt_password = null; } + dt_opassword = null; + dt_mpassword = null; + set_status("green", "OK", "Link Updated"); + window.removeEventListener('beforeunload', onBeforeUnload); } else { - dt_epassword = null; - dt_password = null; + apply_dt(true); + delete_pending = false; + document.title = 'JinjaFx [unsaved]'; + dirty = true; + set_status("green", "OK", "Link Deleted", 10000); + window.addEventListener('beforeunload', onBeforeUnload); + clear_wait(); + return false; } - dt_opassword = null; - dt_mpassword = null; - set_status("green", "OK", "Link Updated"); - window.removeEventListener('beforeunload', onBeforeUnload); } else { window.removeEventListener('beforeunload', onBeforeUnload); @@ -771,6 +798,7 @@ function getStatusText(code) { } else if (this.status == 401) { protect_action = 2; + delete_pending = dflag; document.getElementById('lb_protect').innerHTML = 'DataTemplate ' + this.getResponseHeader('X-Dt-Authentication') + ' Passsword'; new bootstrap.Modal(document.getElementById('protect_input'), { keyboard: false @@ -784,6 +812,7 @@ function getStatusText(code) { var sT = (this.statusText.length == 0) ? getStatusText(this.status) : this.statusText; set_status("darkred", "HTTP ERROR " + this.status, sT); } + delete_pending = false; clear_wait(); }; @@ -799,16 +828,21 @@ function getStatusText(code) { xHR.timeout = 10000; xHR.setRequestHeader("Content-Type", "application/json"); - var rd = JSON.stringify(dt); - if (rd.length > 2048 * 1024) { - set_status("darkred", "ERROR", 'Content Too Large'); - } - else if (rd.length > 1024) { - xHR.setRequestHeader("Content-Encoding", "gzip"); - xHR.send(pako.gzip(rd)); + if (!dflag) { + var rd = JSON.stringify(dt); + if (rd.length > 2048 * 1024) { + set_status("darkred", "ERROR", 'Content Too Large'); + } + else if (rd.length > 1024) { + xHR.setRequestHeader("Content-Encoding", "gzip"); + xHR.send(pako.gzip(rd)); + } + else { + xHR.send(rd); + } } else { - xHR.send(rd); + xHR.send('{}'); } } @@ -860,7 +894,10 @@ function getStatusText(code) { dt_encrypted = false; } - if (dt.hasOwnProperty('dataset')) { + if (qs.hasOwnProperty('ds')) { + load_datatemplate(dt['dt'], qs, qs['ds']); + } + else if (dt.hasOwnProperty('dataset')) { load_datatemplate(dt['dt'], qs, dt['dataset']); } else { @@ -872,7 +909,6 @@ function getStatusText(code) { document.getElementById('get').classList.add('d-none'); document.getElementById('mdd').disabled = false; - document.getElementById('protect').classList.remove('disabled'); if (dt.hasOwnProperty('dt_password') || dt.hasOwnProperty('dt_mpassword')) { document.getElementById('protect_text').innerHTML = 'Update Protection'; dt_protected = true; @@ -964,6 +1000,7 @@ function getStatusText(code) { document.getElementById('get2').onclick = function() { jinjafx('get_link'); }; document.getElementById('update').onclick = function() { jinjafx('update_link'); }; document.getElementById('protect').onclick = function() { jinjafx('protect'); }; + document.getElementById('delete').onclick = function() { jinjafx('delete'); }; if (window.crypto.subtle) { document.getElementById('encrypt').classList.remove('d-none'); @@ -989,7 +1026,7 @@ function getStatusText(code) { var obj = jsyaml.load(contents, jsyaml_schema); if (obj != null) { pending_dt = obj['dt']; - apply_dt(); + apply_dt(false); return true; } } @@ -1014,7 +1051,7 @@ function getStatusText(code) { var obj = jsyaml.load(e2.target.result, jsyaml_schema); if (obj != null) { pending_dt = obj['dt']; - apply_dt(); + apply_dt(false); return true; } } @@ -1538,7 +1575,7 @@ function getStatusText(code) { try_to_load(); } else { - update_link(dt_id); + update_link(dt_id, delete_pending); } } else { @@ -1770,15 +1807,34 @@ function getStatusText(code) { } }; }); - + if (window.location.pathname.startsWith('/dt/') && (window.location.pathname.length > 4)) { - qs['dt'] = decodeURIComponent(window.location.pathname.substr(4)); - + var e = window.location.pathname.substr(1).split('/'); + qs['dt'] = decodeURIComponent(e[1]); + + if (e.length > 2) { + qs['ds'] = decodeURIComponent(e[2]); + } + } + + if (window.location.search.length > 1) { + var v = window.location.search.substr(1).split('&'); + + for (var i = 0; i < v.length; i++) { + var p = v[i].split('='); + + if (!qs.hasOwnProperty(p[0].toLowerCase())) { + qs[p[0].toLowerCase()] = decodeURIComponent(p.length > 1 ? p[1] : ''); + } + } + } + + if (qs.hasOwnProperty('dt')) { if (document.getElementById('get_link').value != 'false') { try_to_load(); - + document.getElementById('lbuttons').classList.remove('d-none'); - + if (fe != window.cmData) { onDataBlur(); } @@ -1790,44 +1846,15 @@ function getStatusText(code) { loaded = true; } } - else if (window.location.href.indexOf('?') > -1) { - var v = window.location.href.substr(window.location.href.indexOf('?') + 1).split('&'); - - for (var i = 0; i < v.length; i++) { - var p = v[i].split('='); - qs[p[0].toLowerCase()] = decodeURIComponent(p.length > 1 ? p[1] : ''); - } - - if (qs.hasOwnProperty('dt')) { - if (document.getElementById('get_link').value != 'false') { - try_to_load(); - - document.getElementById('lbuttons').classList.remove('d-none'); - - if (fe != window.cmData) { - onDataBlur(); - } - } - else { - set_status("darkred", "HTTP ERROR 503", "Service Unavailable"); - reset_location(''); - document.getElementById('buttons').classList.remove('d-none'); - loaded = true; - } - } - else { - reset_location(''); - document.getElementById('buttons').classList.remove('d-none'); - loaded = true; - } - } else { + reset_location(''); + document.getElementById('buttons').classList.remove('d-none'); + document.getElementById('stemplates').style.visibility = 'hidden'; + document.getElementById('template_info').style.visibility = 'visible'; + if (document.getElementById('get_link').value != 'false') { document.getElementById('lbuttons').classList.remove('d-none'); } - document.getElementById('stemplates').style.visibility = 'hidden'; - document.getElementById('template_info').style.visibility = 'visible'; - document.getElementById('buttons').classList.remove('d-none'); loaded = true; } } @@ -1931,8 +1958,10 @@ function getStatusText(code) { } } - function apply_dt() { - load_datatemplate(pending_dt, null, null); + function apply_dt(dflag) { + if (!dflag) { + load_datatemplate(pending_dt, null, null); + } reset_location(''); dt_id = ''; dt_password = null; @@ -1942,9 +1971,10 @@ function getStatusText(code) { input_form = null; document.getElementById('update').classList.add('d-none'); document.getElementById('get').classList.remove('d-none'); - document.getElementById('mdd').disabled = true; - document.getElementById('protect').classList.add('disabled'); - document.getElementById('protect').innerHTML = 'Protect Link'; + document.getElementById('protect_text').innerHTML = 'Protect Link'; + setTimeout(function() { + document.getElementById('mdd').disabled = true; + }, 50); } function onPasteOrDrop(e, obj, target) { @@ -1956,11 +1986,11 @@ function getStatusText(code) { if (dirty) { if (confirm("Are You Sure?") === true) { - apply_dt(); + apply_dt(false); } } else { - apply_dt(); + apply_dt(false); } } } @@ -2024,9 +2054,6 @@ function getStatusText(code) { if (tinfo) { if (editor == window.cmTemplate) { remove_info(); - //document.getElementById('template_info').classList.add('fade-out'); - //document.getElementById('template_info').style.zIndex = -1000; - //document.getElementById('stemplates').style.visibility = 'visible'; tinfo = false; } } @@ -2047,6 +2074,12 @@ function getStatusText(code) { var data = _dt.datasets[ds].hasOwnProperty("data") ? _dt.datasets[ds].data : ""; var vars = _dt.datasets[ds].hasOwnProperty("vars") ? _dt.datasets[ds].vars : ""; datasets[ds] = [CodeMirror.Doc(data, 'data'), CodeMirror.Doc(vars, 'yaml')]; + + if (_ds != null) { + if (ds.toLowerCase() == _ds.toLowerCase()) { + _ds = ds; + } + } }); if ((_ds == null) || !datasets.hasOwnProperty(_ds)) { diff --git a/www/logs.html b/www/logs.html index 1ed4487..f5dd9aa 100644 --- a/www/logs.html +++ b/www/logs.html @@ -9,7 +9,7 @@ - +