From c7ed844c7662c00c95a73f6500989ce1ac656699 Mon Sep 17 00:00:00 2001 From: Risto Lahtela <24460436+Rsl1122@users.noreply.github.com> Date: Tue, 12 Jan 2021 17:11:12 +0200 Subject: [PATCH] View options added to the query --- .../plan/delivery/formatting/Formatters.java | 6 ++ .../time/JavascriptDateFormatter.java | 37 +++++++++ .../resolver/json/FiltersJSONResolver.java | 79 ++++++++++++++---- .../resolver/json/QueryJSONResolver.java | 17 +++- .../database/queries/filter/QueryFilters.java | 9 ++- .../filter/filters/AllPlayersFilter.java | 58 +++++++++++++ .../filter/filters/DateRangeFilter.java | 12 +-- .../playertable/QueryTablePlayersQuery.java | 20 +++-- .../resources/assets/plan/web/js/query.js | 81 +++++++++++-------- .../assets/plan/web/js/sb-admin-2.js | 2 +- .../main/resources/assets/plan/web/query.html | 15 +++- 11 files changed, 267 insertions(+), 69 deletions(-) create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/formatting/time/JavascriptDateFormatter.java create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/AllPlayersFilter.java diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/formatting/Formatters.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/formatting/Formatters.java index d147fa950..24ede4a75 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/formatting/Formatters.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/formatting/Formatters.java @@ -42,6 +42,7 @@ public class Formatters { private final DayFormatter dayLongFormatter; private final SecondFormatter secondLongFormatter; private final ClockFormatter clockLongFormatter; + private final JavascriptDateFormatter javascriptDateFormatter; private final ISO8601NoClockFormatter iso8601NoClockLongFormatter; private final ISO8601NoClockTZIndependentFormatter iso8601NoClockTZIndependentFormatter; @@ -57,6 +58,7 @@ public class Formatters { dayLongFormatter = new DayFormatter(config, locale); clockLongFormatter = new ClockFormatter(config, locale); secondLongFormatter = new SecondFormatter(config, locale); + javascriptDateFormatter = new JavascriptDateFormatter(config, locale); iso8601NoClockLongFormatter = new ISO8601NoClockFormatter(config, locale); iso8601NoClockTZIndependentFormatter = new ISO8601NoClockTZIndependentFormatter(); @@ -109,6 +111,10 @@ public class Formatters { return iso8601NoClockFormatter; } + public Formatter javascriptDateFormatterLong() { + return javascriptDateFormatter; + } + public Formatter iso8601NoClockLong() { return iso8601NoClockLongFormatter; } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/formatting/time/JavascriptDateFormatter.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/formatting/time/JavascriptDateFormatter.java new file mode 100644 index 000000000..57f94a66e --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/formatting/time/JavascriptDateFormatter.java @@ -0,0 +1,37 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.delivery.formatting.time; + +import com.djrapitops.plan.settings.config.PlanConfig; +import com.djrapitops.plan.settings.locale.Locale; + +/** + * Formats epoch milliseconds to the date format Javascript Date constructor expects. + * + * @author Rsl1122 + */ +public class JavascriptDateFormatter extends DateFormatter { + + public JavascriptDateFormatter(PlanConfig config, Locale locale) { + super(config, locale); + } + + @Override + public String apply(Long epochMs) { + return format(epochMs, "dd/MM/yyyy kk:mm"); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java index 9eb2ad529..0d6dc17df 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/FiltersJSONResolver.java @@ -16,6 +16,8 @@ */ package com.djrapitops.plan.delivery.webserver.resolver.json; +import com.djrapitops.plan.delivery.formatting.Formatter; +import com.djrapitops.plan.delivery.formatting.Formatters; import com.djrapitops.plan.delivery.web.resolver.MimeType; import com.djrapitops.plan.delivery.web.resolver.Resolver; import com.djrapitops.plan.delivery.web.resolver.Response; @@ -23,7 +25,7 @@ import com.djrapitops.plan.delivery.web.resolver.request.Request; import com.djrapitops.plan.delivery.web.resolver.request.WebUser; import com.djrapitops.plan.storage.database.queries.filter.Filter; import com.djrapitops.plan.storage.database.queries.filter.QueryFilters; -import com.djrapitops.plan.utilities.java.Maps; +import org.apache.commons.lang3.StringUtils; import javax.inject.Inject; import javax.inject.Singleton; @@ -31,17 +33,21 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.TimeUnit; @Singleton public class FiltersJSONResolver implements Resolver { private final QueryFilters filters; + private final Formatters formatters; @Inject public FiltersJSONResolver( - QueryFilters filters + QueryFilters filters, + Formatters formatters ) { this.filters = filters; + this.formatters = formatters; } @Override @@ -58,21 +64,64 @@ public class FiltersJSONResolver implements Resolver { private Response getResponse() { return Response.builder() .setMimeType(MimeType.JSON) - .setJSONContent(Maps.builder(String.class, Object.class) - .put("filters", serializeFilters()) - .build()) - .build(); + .setJSONContent(new FilterResponseJSON( + filters.getFilters(), + new ViewJSON(formatters) + )).build(); } - private List> serializeFilters() { - List> filterList = new ArrayList<>(); - for (Map.Entry entry : filters.getFilters().entrySet()) { - filterList.add(Maps.builder(String.class, Object.class) - .put("kind", entry.getKey()) - .put("options", entry.getValue().getOptions()) - .put("expectedParameters", entry.getValue().getExpectedParameters()) - .build()); + /** + * JSON serialization class. + */ + static class FilterResponseJSON { + final List filters; + final ViewJSON view; + + public FilterResponseJSON(Map filtersByKind, ViewJSON view) { + this.filters = new ArrayList<>(); + for (Map.Entry entry : filtersByKind.entrySet()) { + filters.add(new FilterJSON(entry.getKey(), entry.getValue())); + } + this.view = view; + } + } + + /** + * JSON serialization class. + */ + static class FilterJSON { + final String kind; + final Map options; + final String[] expectedParameters; + + public FilterJSON(String kind, Filter filter) { + this.kind = kind; + this.options = filter.getOptions(); + this.expectedParameters = filter.getExpectedParameters(); + } + } + + /** + * JSON serialization class. + */ + static class ViewJSON { + final String afterDate; + final String afterTime; + final String beforeDate; + final String beforeTime; + + public ViewJSON(Formatters formatters) { + long now = System.currentTimeMillis(); + long monthAgo = now - TimeUnit.DAYS.toMillis(30); + + Formatter formatter = formatters.javascriptDateFormatterLong(); + String[] after = StringUtils.split(formatter.apply(monthAgo), " "); + String[] before = StringUtils.split(formatter.apply(now), " "); + + this.afterDate = after[0]; + this.afterTime = after[1]; + this.beforeDate = before[0]; + this.beforeTime = before[1]; } - return filterList; } } diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java index fdc2fa781..6df426699 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/json/QueryJSONResolver.java @@ -35,11 +35,14 @@ import com.djrapitops.plan.storage.database.queries.filter.FilterQuery; import com.djrapitops.plan.storage.database.queries.filter.QueryFilters; import com.djrapitops.plan.storage.database.queries.objects.playertable.QueryTablePlayersQuery; import com.djrapitops.plan.utilities.java.Maps; +import com.google.gson.Gson; import javax.inject.Inject; import javax.inject.Singleton; import java.io.IOException; import java.net.URLDecoder; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.*; @Singleton @@ -81,6 +84,7 @@ public class QueryJSONResolver implements Resolver { private Response getResponse(Request request) { String q = request.getQuery().get("q").orElseThrow(() -> new BadRequestException("'q' parameter not set (expecting json array)")); String view = request.getQuery().get("view").orElseThrow(() -> new BadRequestException("'view' parameter not set (expecting json object {afterDate, afterTime, beforeDate, beforeTime})")); + try { q = URLDecoder.decode(q, "UTF-8"); List queries = FilterQuery.parse(q); @@ -90,23 +94,30 @@ public class QueryJSONResolver implements Resolver { .put("path", result.getResultPath()) .build(); if (!result.isEmpty()) { - json.put("data", getDataFor(result.getResultUUIDs())); + json.put("data", getDataFor(result.getResultUUIDs(), view)); } return Response.builder() .setMimeType(MimeType.JSON) .setJSONContent(json) .build(); + } catch (ParseException e) { + throw new BadRequestException("'view' date format was incorrect (expecting afterDate dd/mm/yyyy, afterTime hh:mm, beforeDate dd/mm/yyyy, beforeTime hh:mm}): " + e.getMessage()); } catch (IOException e) { throw new BadRequestException("Failed to parse json: '" + q + "'" + e.getMessage()); } } - private Map getDataFor(Set playerUUIDs) { + private Map getDataFor(Set playerUUIDs, String view) throws ParseException { + FiltersJSONResolver.ViewJSON viewJSON = new Gson().fromJson(view, FiltersJSONResolver.ViewJSON.class); + SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy kk:mm"); + long after = dateFormat.parse(viewJSON.afterDate + " " + viewJSON.afterTime).getTime(); + long before = dateFormat.parse(viewJSON.beforeDate + " " + viewJSON.beforeTime).getTime(); + Database database = dbSystem.getDatabase(); return Maps.builder(String.class, Object.class) .put("players", new PlayersTableJSONCreator( - database.query(new QueryTablePlayersQuery(playerUUIDs, System.currentTimeMillis(), config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD))), + database.query(new QueryTablePlayersQuery(playerUUIDs, after, before, config.get(TimeSettings.ACTIVE_PLAY_THRESHOLD))), Collections.emptyMap(), config.get(DisplaySettings.OPEN_PLAYER_LINKS_IN_NEW_TAB), formatters, locale diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/QueryFilters.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/QueryFilters.java index e88852806..d8a7fd8e4 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/QueryFilters.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/QueryFilters.java @@ -17,6 +17,7 @@ package com.djrapitops.plan.storage.database.queries.filter; import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException; +import com.djrapitops.plan.storage.database.queries.filter.filters.AllPlayersFilter; import javax.inject.Inject; import javax.inject.Singleton; @@ -32,9 +33,14 @@ import java.util.*; public class QueryFilters { private final Map filters; + private final AllPlayersFilter allPlayersFilter; @Inject - public QueryFilters(Set filters) { + public QueryFilters( + Set filters, + AllPlayersFilter allPlayersFilter + ) { + this.allPlayersFilter = allPlayersFilter; this.filters = new HashMap<>(); put(filters); } @@ -58,6 +64,7 @@ public class QueryFilters { */ public Filter.Result apply(List filterQueries) { Filter.Result current = null; + if (filterQueries.isEmpty()) return allPlayersFilter.apply(null); for (FilterQuery filterQuery : filterQueries) { current = apply(current, filterQuery); if (current != null && current.isEmpty()) break; diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/AllPlayersFilter.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/AllPlayersFilter.java new file mode 100644 index 000000000..d1e00ab7c --- /dev/null +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/AllPlayersFilter.java @@ -0,0 +1,58 @@ +/* + * This file is part of Player Analytics (Plan). + * + * Plan is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License v3 as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Plan is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Plan. If not, see . + */ +package com.djrapitops.plan.storage.database.queries.filter.filters; + +import com.djrapitops.plan.storage.database.DBSystem; +import com.djrapitops.plan.storage.database.queries.filter.Filter; +import com.djrapitops.plan.storage.database.queries.filter.FilterQuery; +import com.djrapitops.plan.storage.database.queries.objects.UserIdentifierQueries; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Set; +import java.util.UUID; + +/** + * Special filter only used in cases where no filters are specified. + * + * @author Rsl1122 + */ +@Singleton +public class AllPlayersFilter implements Filter { + + private final DBSystem dbSystem; + + @Inject + public AllPlayersFilter(DBSystem dbSystem) { + this.dbSystem = dbSystem; + } + + @Override + public String getKind() { + return "all"; + } + + @Override + public String[] getExpectedParameters() { + return new String[0]; + } + + @Override + public Set getMatchingUUIDs(FilterQuery query) { + return dbSystem.getDatabase().query(UserIdentifierQueries.fetchAllPlayerUUIDs()); + } +} diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/DateRangeFilter.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/DateRangeFilter.java index 71eb03dd2..638c8a1b1 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/DateRangeFilter.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/filter/filters/DateRangeFilter.java @@ -40,10 +40,10 @@ public abstract class DateRangeFilter implements Filter { @Override public String[] getExpectedParameters() { return new String[]{ - "dateAfter", - "timeAfter", - "dateBefore", - "timeBefore" + "afterDate", + "afterTime", + "beforeDate", + "beforeTime" }; } @@ -61,11 +61,11 @@ public abstract class DateRangeFilter implements Filter { } protected long getAfter(FilterQuery query) { - return getTime(query, "dateAfter", "timeAfter"); + return getTime(query, "afterDate", "afterTime"); } protected long getBefore(FilterQuery query) { - return getTime(query, "dateBefore", "timeBefore"); + return getTime(query, "beforeDate", "beforeTime"); } private long getTime(FilterQuery query, String dateKey, String timeKey) { diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/playertable/QueryTablePlayersQuery.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/playertable/QueryTablePlayersQuery.java index ada5eb51e..d78e3765f 100644 --- a/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/playertable/QueryTablePlayersQuery.java +++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/database/queries/objects/playertable/QueryTablePlayersQuery.java @@ -46,19 +46,21 @@ import static com.djrapitops.plan.storage.database.sql.building.Sql.*; public class QueryTablePlayersQuery implements Query> { private final Collection playerUUIDs; - private final long date; + private final long afterDate; + private final long beforeDate; private final long activeMsThreshold; /** * Create a new query. * * @param playerUUIDs UUIDs of the players in the query - * @param date Date used for Activity Index calculation + * @param beforeDate Date used for Activity Index calculation * @param activeMsThreshold Playtime threshold for Activity Index calculation */ - public QueryTablePlayersQuery(Collection playerUUIDs, long date, long activeMsThreshold) { + public QueryTablePlayersQuery(Collection playerUUIDs, long afterDate, long beforeDate, long activeMsThreshold) { this.playerUUIDs = playerUUIDs; - this.date = date; + this.afterDate = afterDate; + this.beforeDate = beforeDate; this.activeMsThreshold = activeMsThreshold; } @@ -88,7 +90,9 @@ public class QueryTablePlayersQuery implements Query> { "COUNT(1) as count," + "SUM(" + SessionsTable.SESSION_END + '-' + SessionsTable.SESSION_START + ") as playtime" + FROM + SessionsTable.TABLE_NAME + " s" + - WHERE + "s." + SessionsTable.USER_UUID + + WHERE + "s." + SessionsTable.SESSION_START + ">=?" + + AND + "s." + SessionsTable.SESSION_END + "<=?" + + AND + "s." + SessionsTable.USER_UUID + uuidsInSet + GROUP_BY + "s." + SessionsTable.USER_UUID; @@ -114,7 +118,9 @@ public class QueryTablePlayersQuery implements Query> { return db.query(new QueryStatement>(selectBaseUsers, 1000) { @Override public void prepare(PreparedStatement statement) throws SQLException { - NetworkActivityIndexQueries.setSelectActivityIndexSQLParameters(statement, 1, activeMsThreshold, date); + statement.setLong(1, afterDate); + statement.setLong(2, beforeDate); + NetworkActivityIndexQueries.setSelectActivityIndexSQLParameters(statement, 3, activeMsThreshold, beforeDate); } @Override @@ -129,7 +135,7 @@ public class QueryTablePlayersQuery implements Query> { .lastSeen(set.getLong("last_seen")) .sessionCount(set.getInt("count")) .playtime(set.getLong("playtime")) - .activityIndex(new ActivityIndex(set.getDouble("activity_index"), date)); + .activityIndex(new ActivityIndex(set.getDouble("activity_index"), beforeDate)); if (set.getBoolean(UserInfoTable.BANNED)) { player.banned(); } diff --git a/Plan/common/src/main/resources/assets/plan/web/js/query.js b/Plan/common/src/main/resources/assets/plan/web/js/query.js index bd2b12d47..90a843a72 100644 --- a/Plan/common/src/main/resources/assets/plan/web/js/query.js +++ b/Plan/common/src/main/resources/assets/plan/web/js/query.js @@ -4,11 +4,11 @@ let filterCount = 0; id: "DOM id", options... }*/ -const filterView = { - dateAfter: null, - timeAfter: null, - dateBefore: null, - timeBefore: null +let filterView = { + afterDate: null, + afterTime: null, + beforeDate: null, + beforeTime: null }; const filterQuery = []; @@ -102,10 +102,10 @@ class BetweenDateFilter extends Filter { super(kind); this.id = id; this.label = label; - this.dateAfter = options.after[0]; - this.timeAfter = options.after[1]; - this.dateBefore = options.before[0]; - this.timeBefore = options.before[1]; + this.afterDate = options.after[0]; + this.afterTime = options.after[1]; + this.beforeDate = options.before[0]; + this.beforeTime = options.before[1]; } render(filterCount) { @@ -116,20 +116,20 @@ class BetweenDateFilter extends Filter { `
` + `
` + `
` + - `` + + `` + `
` + `
` + `
` + - `` + + `` + `
` + `
` + `
` + `
` + - `` + + `` + `
` + `
` + `
` + - `` + + `` + `
` + `
` ); @@ -139,10 +139,10 @@ class BetweenDateFilter extends Filter { return { kind: this.kind, parameters: { - dateAfter: this.dateAfter, - timeAfter: this.timeAfter, - dateBefore: this.dateBefore, - timeBefore: this.timeBefore + afterDate: this.afterDate, + afterTime: this.afterTime, + beforeDate: this.beforeDate, + beforeTime: this.beforeTime } } } @@ -274,7 +274,7 @@ function performQuery() { if (json) console.log(json); if (error) console.error(error); - renderDataResultScreen(); + renderDataResultScreen(json.data.players.data.length); $('.player-table').DataTable({ responsive: true, @@ -282,24 +282,41 @@ function performQuery() { data: json.data.players.data, order: [[5, "desc"]] }) + + const activityIndexHeader = document.querySelector("#DataTables_Table_0 thead th:nth-of-type(2)"); + const lastSeenHeader = document.querySelector("#DataTables_Table_0 thead th:nth-of-type(6)"); + + activityIndexHeader.innerHTML += ` (${filterView.beforeDate})` + lastSeenHeader.innerHTML += ` (view)` }); } -function renderDataResultScreen() { - document.querySelector('#content .tab').innerHTML += - `
-
-
-
-
- - - - -
Loading..
+function renderDataResultScreen(resultCount) { + document.querySelector('#content .tab').innerHTML = + `
+ +
+

Plan · + Query Results

+

(matched ${resultCount} players)

+
+
+
+
+
+
+ View: ${filterView.afterDate} - ${filterView.beforeDate}
+
+
+ + + + +
Loading..
+
-
-
`; +
`; } \ No newline at end of file diff --git a/Plan/common/src/main/resources/assets/plan/web/js/sb-admin-2.js b/Plan/common/src/main/resources/assets/plan/web/js/sb-admin-2.js index d85fa8a56..e0d9e7fb5 100644 --- a/Plan/common/src/main/resources/assets/plan/web/js/sb-admin-2.js +++ b/Plan/common/src/main/resources/assets/plan/web/js/sb-admin-2.js @@ -43,7 +43,7 @@ function openPage() { // Prepare tabs for display content.style.transform = "translate3d(0px,0px,0)"; -content.style.width = (tabCount * 100) + "%"; +content.style.width = (Math.max(100, tabCount * 100)) + "%"; content.style.opacity = "1"; for (let tab of tabs) { tab.style.width = (100 / tabCount) + "%"; diff --git a/Plan/common/src/main/resources/assets/plan/web/query.html b/Plan/common/src/main/resources/assets/plan/web/query.html index c5b83f4c4..e09ac021e 100644 --- a/Plan/common/src/main/resources/assets/plan/web/query.html +++ b/Plan/common/src/main/resources/assets/plan/web/query.html @@ -93,7 +93,7 @@
@@ -105,7 +105,7 @@
@@ -119,7 +119,7 @@
@@ -130,7 +130,7 @@
@@ -325,6 +325,13 @@ jsonRequest("./v1/filters", function (json, error) { filters.push(...json.filters); + filterView = json.view; + + document.getElementById('viewFromDateField').setAttribute('placeholder', json.view.afterDate); + document.getElementById('viewFromTimeField').setAttribute('placeholder', json.view.afterTime); + document.getElementById('viewToDateField').setAttribute('placeholder', json.view.beforeDate); + document.getElementById('viewToTimeField').setAttribute('placeholder', json.view.beforeTime); + let filterElements = ''; for (let i = 0; i < filters.length; i++) { filterElements += createFilterSelector('#filters', i, filters[i]);