Dashboards
++ Tạo và chỉnh sửa dashboard cho robot. + +
+Chưa có widget. Bấm «Thêm widget» để bắt đầu.
-Tạo dashboard
++ Tạo dashboard mới trên robot. + +
+—
+Chưa có widget. Phase B sẽ thêm designer đầy đủ.
diff --git a/README.md b/README.md index 978da9d..cd015ae 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# LiDAR Manager Web (Test3) +# Robot App Web (Test3) Chức năng: - Đăng ký danh sách cảm biến LiDAR (tên, ip, port) diff --git a/data/robot_runtime.json b/data/robot_runtime.json index f694bbd..9e8b8ec 100644 --- a/data/robot_runtime.json +++ b/data/robot_runtime.json @@ -7,7 +7,7 @@ "health": "ok", "joystick_engaged": false, "joystick_speed": "fast", - "message": "Robot paused", - "motion": "paused", - "updated_at": "2026-06-16T03:40:34Z" + "message": "Waiting for new missions...", + "motion": "running", + "updated_at": "2026-06-16T10:33:19Z" } \ No newline at end of file diff --git a/www/dashboard.js b/www/dashboard.js index a91937c..f4ddae1 100644 --- a/www/dashboard.js +++ b/www/dashboard.js @@ -1,16 +1,27 @@ (() => { - const STORAGE_KEY = "phenikaax_dashboard_v1"; - - function widgetTypeLabel(type) { - return t(`dashboard.widget.${type}`) || type; - } + const STORAGE_KEY_V3 = "phenikaax_dashboard_v3"; + const STORAGE_KEY_V2 = "phenikaax_dashboard_v2"; + const PAGE_SIZE = 10; + const DEFAULT_ID = "dashboard_default"; const el = (id) => document.getElementById(id); const t = (key, vars) => window.I18n?.t(key, vars) ?? key; + + const listViewEl = el("dashboardListView"); + const createViewEl = el("dashboardCreateView"); + const designerViewEl = el("dashboardDesignerView"); const gridEl = el("dashboardGrid"); - const emptyEl = el("dashboardEmpty"); + const designerEmptyEl = el("dashboardDesignerEmpty"); + const tableBodyEl = el("dashboardTableBody"); + const tableWrapEl = el("dashboardTableWrap"); + const filterInputEl = el("dashboardFilterInput"); + const listCountEl = el("dashboardListCount"); + const pageLabelEl = el("dashboardPageLabel"); + const designerTitleEl = el("dashboardDesignerTitle"); + const editDialogEl = el("dashboardEditDialog"); + const permissionsDialogEl = el("dashboardPermissionsDialog"); const addDialogEl = el("dashboardAddWidgetDialog"); - const editDialogEl = el("dashboardEditWidgetDialog"); + const editWidgetDialogEl = el("dashboardEditWidgetDialog"); const addTypeEl = el("dashboardWidgetType"); const addFieldsEl = el("dashboardAddWidgetFields"); const editFieldsEl = el("dashboardEditWidgetFields"); @@ -18,50 +29,57 @@ const editWidgetTypeEl = el("dashboardEditWidgetType"); const store = { - widgets: [], + dashboards: [], + activeDashboardId: null, + view: "list", + filter: "", + page: 1, editMode: false, pollActive: false, queueUnsub: null, + userGroups: [], }; - function newId() { - if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID(); - return `w_${Date.now().toString(36)}`; + function widgetTypeLabel(type) { + return t(`dashboard.widget.${type}`) || type; + } + + function newId(prefix = "d") { + if (typeof crypto !== "undefined" && crypto.randomUUID) return `${prefix}_${crypto.randomUUID()}`; + return `${prefix}_${Date.now().toString(36)}`; } function missions() { return window.MissionsApp || null; } - function loadStore() { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) { - bootstrapDefaults(); - return; - } - const data = JSON.parse(raw); - store.widgets = Array.isArray(data.widgets) ? data.widgets : []; - if (!store.widgets.length) bootstrapDefaults(); - } catch { - bootstrapDefaults(); - } + function currentUser() { + return window.AuthApp?.getUser?.() || null; } - function persistStore() { - localStorage.setItem(STORAGE_KEY, JSON.stringify({ widgets: store.widgets })); + function canEditDashboardsModule() { + const user = currentUser(); + if (!user?.permissions) return true; + const level = user.permissions.dashboard; + return level === "write" || level === undefined; } - function bootstrapDefaults() { - const m = missions()?.getMissions?.() || []; - const firstId = m[0]?.id || ""; - store.widgets = [ - { id: newId(), type: "mission_button", mission_id: firstId, title: "" }, - { id: newId(), type: "mission_group", group: "Missions", title: "" }, - { id: newId(), type: "mission_queue", title: "" }, - { id: newId(), type: "pause_continue", title: "" }, - ]; - persistStore(); + function activeDashboard() { + return store.dashboards.find((d) => d.id === store.activeDashboardId) || null; + } + + function dashboardCanEdit(dashboard) { + if (!dashboard) return false; + if (!canEditDashboardsModule()) return false; + const user = currentUser(); + if (!user) return true; + const groups = Array.isArray(dashboard.editGroups) ? dashboard.editGroups : []; + if (!groups.length) return true; + return groups.includes(user.group_id); + } + + function normalizeStr(value) { + return String(value || "").trim(); } function escapeHtml(str) { @@ -72,6 +90,284 @@ .replace(/"/g, """); } + function activeWidgets() { + return activeDashboard()?.widgets || []; + } + + function setActiveWidgets(widgets) { + const db = activeDashboard(); + if (db) db.widgets = widgets; + } + + function bootstrapDefaultDashboard(widgets = []) { + store.dashboards = [ + { + id: DEFAULT_ID, + name: "Default Dashboard", + createdBy: t("dashboard.createdBy.system"), + createdByUser: null, + isDefault: true, + editGroups: ["group_administrators", "group_distributors", "group_users"], + widgets: Array.isArray(widgets) ? widgets : [], + }, + ]; + store.activeDashboardId = DEFAULT_ID; + persistStore(); + } + + function migrateFromV2() { + try { + const raw = localStorage.getItem(STORAGE_KEY_V2); + if (!raw) return false; + const data = JSON.parse(raw); + const widgets = Array.isArray(data.widgets) ? data.widgets : []; + bootstrapDefaultDashboard(widgets); + return true; + } catch { + return false; + } + } + + function loadStore() { + try { + const raw = localStorage.getItem(STORAGE_KEY_V3); + if (!raw) { + if (!migrateFromV2()) bootstrapDefaultDashboard(); + return; + } + const data = JSON.parse(raw); + store.dashboards = Array.isArray(data.dashboards) ? data.dashboards : []; + store.activeDashboardId = data.activeDashboardId || store.dashboards[0]?.id || null; + if (!store.dashboards.length) bootstrapDefaultDashboard(); + else if (!store.activeDashboardId) store.activeDashboardId = store.dashboards[0].id; + } catch { + bootstrapDefaultDashboard(); + } + } + + function persistStore() { + localStorage.setItem( + STORAGE_KEY_V3, + JSON.stringify({ + dashboards: store.dashboards, + activeDashboardId: store.activeDashboardId, + }) + ); + } + + async function loadUserGroups() { + try { + const res = await fetch("/api/user_groups", { credentials: "include" }); + if (!res.ok) return; + const data = await res.json(); + store.userGroups = Array.isArray(data.groups) ? data.groups : []; + } catch { + store.userGroups = []; + } + } + + function filteredDashboards() { + const q = normalizeStr(store.filter).toLowerCase(); + const list = [...store.dashboards]; + list.sort((a, b) => { + if (a.isDefault && !b.isDefault) return -1; + if (!a.isDefault && b.isDefault) return 1; + return a.name.localeCompare(b.name); + }); + if (!q) return list; + return list.filter((d) => d.name.toLowerCase().includes(q)); + } + + function paginatedDashboards() { + const all = filteredDashboards(); + const totalPages = Math.max(1, Math.ceil(all.length / PAGE_SIZE)); + if (store.page > totalPages) store.page = totalPages; + if (store.page < 1) store.page = 1; + const start = (store.page - 1) * PAGE_SIZE; + return { all, pageItems: all.slice(start, start + PAGE_SIZE), totalPages }; + } + + function renderPermissionsChecklist(container, selected = []) { + if (!container) return; + container.replaceChildren(); + const selectedSet = new Set(selected); + store.userGroups.forEach((group) => { + const label = document.createElement("label"); + const input = document.createElement("input"); + input.type = "checkbox"; + input.value = group.id; + input.checked = selectedSet.has(group.id); + label.appendChild(input); + label.appendChild(document.createTextNode(group.name || group.id)); + container.appendChild(label); + }); + if (!store.userGroups.length) { + const note = document.createElement("p"); + note.className = "mutedNote"; + note.textContent = "—"; + container.appendChild(note); + } + } + + function readPermissionsChecklist(container) { + const ids = []; + container?.querySelectorAll('input[type="checkbox"]:checked').forEach((node) => { + ids.push(node.value); + }); + return ids; + } + + function refreshNav() { + window.NavApp?.refreshFlyout?.(); + } + + function setView(view) { + store.view = view; + if (listViewEl) listViewEl.hidden = view !== "list"; + if (createViewEl) createViewEl.hidden = view !== "create"; + if (designerViewEl) designerViewEl.hidden = view !== "designer"; + if (view === "list") { + stopDashboardPoll(); + renderListUI(); + } else if (view === "create") { + stopDashboardPoll(); + } else { + renderDesignerChrome(); + renderDashboard(); + startDashboardPoll(); + } + } + + function setActiveDashboard(id) { + if (!store.dashboards.some((d) => d.id === id)) return; + store.activeDashboardId = id; + persistStore(); + refreshNav(); + } + + function renderListUI() { + const { all, pageItems, totalPages } = paginatedDashboards(); + + if (listCountEl) listCountEl.textContent = t("dashboard.list.itemsFound", { count: all.length }); + if (pageLabelEl) pageLabelEl.textContent = t("dashboard.list.pageOf", { page: store.page, total: totalPages }); + + document.querySelectorAll(".dashboardPageBtn").forEach((btn) => { + const action = btn.dataset.pageAction; + if (action === "first" || action === "prev") btn.disabled = store.page <= 1; + else if (action === "next" || action === "last") btn.disabled = store.page >= totalPages; + }); + + if (tableBodyEl) { + tableBodyEl.replaceChildren(); + if (!pageItems.length) { + const tr = document.createElement("tr"); + tr.className = "dashboardTableEmptyRow"; + tr.innerHTML = `
Tạm dừng / tiếp tục / hủy mission đang chạy trên robot.
`; +${escapeHtml(t("dashboard.widget.pauseHint"))}
`; } } @@ -240,8 +536,8 @@Widget không hỗ trợ.
`; + bodyEl.innerHTML = `${t("dashboard.widget.unsupported")}
`; } card.querySelector("[data-widget-config]")?.addEventListener("click", () => openEditDialog(widget.id)); @@ -271,17 +567,18 @@ function renderDashboard() { if (!gridEl) return; + const widgets = activeWidgets(); gridEl.innerHTML = ""; - if (emptyEl) emptyEl.hidden = store.widgets.length > 0; + if (designerEmptyEl) designerEmptyEl.hidden = widgets.length > 0; gridEl.classList.toggle("dashboardGrid--edit", store.editMode); - store.widgets.forEach((w) => gridEl.appendChild(renderWidget(w))); + widgets.forEach((w) => gridEl.appendChild(renderWidget(w))); gridEl.querySelectorAll(".dashboardWidgetChrome").forEach((n) => { n.hidden = !store.editMode; }); } function refreshDynamicWidgets() { - store.widgets.forEach((widget) => { + activeWidgets().forEach((widget) => { const card = gridEl?.querySelector(`[data-widget-id="${widget.id}"]`); if (!card) return; const bodyEl = card.querySelector(".dashboardWidgetBody"); @@ -290,43 +587,143 @@ }); } - function openAddDialog() { - fillTypeFields(addFieldsEl, addTypeEl.value); - addDialogEl.showModal(); - } - function openEditDialog(widgetId) { - const widget = store.widgets.find((w) => w.id === widgetId); + const widget = activeWidgets().find((w) => w.id === widgetId); if (!widget) return; editWidgetIdEl.value = widget.id; editWidgetTypeEl.value = widgetTypeLabel(widget.type); fillTypeFields(editFieldsEl, widget.type, widget); - editDialogEl.showModal(); + editWidgetDialogEl.showModal(); } function deleteWidget(widgetId) { if (!confirm(t("dashboard.widget.deleteConfirm"))) return; - store.widgets = store.widgets.filter((w) => w.id !== widgetId); + const db = activeDashboard(); + if (!db) return; + db.widgets = db.widgets.filter((w) => w.id !== widgetId); persistStore(); renderDashboard(); - editDialogEl.close(); + editWidgetDialogEl.close(); + } + + function handleNav(section) { + if (section === "dashboard-list") { + setView("list"); + return; + } + if (section.startsWith("dashboard-")) { + const id = section.slice("dashboard-".length); + if (store.dashboards.some((d) => d.id === id)) { + setActiveDashboard(id); + setView("designer"); + } + } + } + + function getNavItems() { + const items = [{ section: "dashboard-list", page: "dashboard", label: t("nav.dashboardsList") }]; + store.dashboards.forEach((dashboard) => { + items.push({ + section: `dashboard-${dashboard.id}`, + page: "dashboard", + label: dashboard.name, + }); + }); + return items; } function bindEvents() { - el("dashboardAddWidgetBtn")?.addEventListener("click", openAddDialog); - el("dashboardEditBtn")?.addEventListener("click", () => { - store.editMode = !store.editMode; - el("dashboardEditBtn").textContent = store.editMode ? t("dashboard.editDone") : t("dashboard.editLayout"); - renderDashboard(); + el("dashboardCreateBtn")?.addEventListener("click", openCreateView); + el("dashboardCreateBackBtn")?.addEventListener("click", closeCreateView); + el("dashboardCreateCancelBtn")?.addEventListener("click", closeCreateView); + el("dashboardCreatePermissionsBtn")?.addEventListener("click", openCreatePermissionsDialog); + el("dashboardPermissionsForm")?.addEventListener("submit", (evt) => { + evt.preventDefault(); + permissionsDialogEl?.close(); + }); + el("dashboardClearFiltersBtn")?.addEventListener("click", () => { + store.filter = ""; + store.page = 1; + if (filterInputEl) filterInputEl.value = ""; + renderListUI(); + }); + filterInputEl?.addEventListener("input", () => { + store.filter = filterInputEl.value; + store.page = 1; + renderListUI(); + }); + + document.getElementById("dashboardPagination")?.addEventListener("click", (evt) => { + const btn = evt.target.closest("[data-page-action]"); + if (!btn || btn.disabled) return; + const { totalPages } = paginatedDashboards(); + const action = btn.dataset.pageAction; + if (action === "first") store.page = 1; + else if (action === "prev") store.page = Math.max(1, store.page - 1); + else if (action === "next") store.page = Math.min(totalPages, store.page + 1); + else if (action === "last") store.page = totalPages; + renderListUI(); + }); + + tableBodyEl?.addEventListener("click", (evt) => { + const btn = evt.target.closest("[data-action]"); + if (!btn) return; + const id = btn.dataset.id; + const action = btn.dataset.action; + if (action === "design") openDesignerFor(id); + else if (action === "edit") openEditDashboardDialog(id); + else if (action === "delete") deleteDashboard(id); + }); + + el("dashboardBackToListBtn")?.addEventListener("click", () => { + setView("list"); + window.NavApp?.syncDashboardSection?.("dashboard-list"); + }); + + el("dashboardCreateForm")?.addEventListener("submit", (evt) => { + evt.preventDefault(); + const name = normalizeStr(el("dashboardCreateName").value); + if (!name) return; + const user = currentUser(); + const dashboard = { + id: newId("dashboard"), + name, + createdBy: user?.group_name || user?.display_name || user?.username || "—", + createdByUser: user?.username || null, + isDefault: false, + editGroups: readPermissionsChecklist(el("dashboardCreatePermissions")), + widgets: [], + }; + store.dashboards.push(dashboard); + store.activeDashboardId = dashboard.id; + persistStore(); + refreshNav(); + openDesignerFor(dashboard.id); + }); + + el("dashboardEditForm")?.addEventListener("submit", (evt) => { + evt.preventDefault(); + const id = el("dashboardEditId").value; + const dashboard = store.dashboards.find((d) => d.id === id); + if (!dashboard || !dashboardCanEdit(dashboard)) return; + dashboard.name = normalizeStr(el("dashboardEditName").value); + dashboard.editGroups = readPermissionsChecklist(el("dashboardEditPermissions")); + persistStore(); + editDialogEl?.close(); + refreshNav(); + if (store.view === "list") renderListUI(); + else renderDesignerChrome(); }); addTypeEl?.addEventListener("change", () => fillTypeFields(addFieldsEl, addTypeEl.value)); el("dashboardAddWidgetForm")?.addEventListener("submit", (evt) => { evt.preventDefault(); + const db = activeDashboard(); + if (!db) return; const type = addTypeEl.value; const fields = readFields(addFieldsEl); - store.widgets.push({ id: newId(), type, ...fields }); + db.widgets.push({ id: newId("w"), type, ...fields }); persistStore(); addDialogEl.close(); renderDashboard(); @@ -335,11 +732,11 @@ el("dashboardEditWidgetForm")?.addEventListener("submit", (evt) => { evt.preventDefault(); const id = editWidgetIdEl.value; - const widget = store.widgets.find((w) => w.id === id); + const widget = activeWidgets().find((w) => w.id === id); if (!widget) return; Object.assign(widget, readFields(editFieldsEl)); persistStore(); - editDialogEl.close(); + editWidgetDialogEl.close(); renderDashboard(); }); @@ -348,6 +745,7 @@ function startDashboardPoll() { if (window.AuthApp && !window.AuthApp.isReady()) return; + if (store.view !== "designer") return; stopDashboardPoll(); missions()?.refreshQueue?.(); store.queueUnsub = missions()?.onQueueUpdate?.(() => refreshDynamicWidgets()); @@ -366,33 +764,47 @@ } } - function init() { + async function init() { loadStore(); + await loadUserGroups(); bindEvents(); - renderDashboard(); + setView("list"); } window.DashboardApp = { init, + getNavItems, + handleNav, onPageShow() { - renderDashboard(); - startDashboardPoll(); + if (store.view === "designer") { + renderDesignerChrome(); + renderDashboard(); + startDashboardPoll(); + } else { + renderListUI(); + } }, onPageHide() { stopDashboardPoll(); }, refresh() { - renderDashboard(); + if (store.view === "list") renderListUI(); + else renderDashboard(); }, }; - function boot() { - init(); + async function boot() { + await init(); + refreshNav(); } + window.addEventListener("lm:locale-change", () => { - renderDashboard(); - const editBtn = el("dashboardEditBtn"); - if (editBtn) editBtn.textContent = store.editMode ? t("dashboard.editDone") : t("dashboard.editLayout"); + if (store.view === "list") renderListUI(); + else { + renderDesignerChrome(); + renderDashboard(); + } + refreshNav(); }); if (window.AuthApp?.isReady()) boot(); diff --git a/www/i18n.js b/www/i18n.js index c5e94d9..c7d6a58 100644 --- a/www/i18n.js +++ b/www/i18n.js @@ -1,12 +1,12 @@ /** - * Central i18n for LiDAR Manager — vi / en. + * Central i18n for Robot App — vi / en. * Static DOM: data-i18n, data-i18n-placeholder, data-i18n-title, data-i18n-aria * Dynamic JS: I18n.t("key") or I18n.t("key", { name: "..." }) */ (() => { const MESSAGES = { vi: { - "app.title": "LiDAR Manager", + "app.title": "Robot App", "app.robotName": "RobotApp", "app.status.ready": "Sẵn sàng", "app.status.reloaded": "Đã tải lại", @@ -66,6 +66,7 @@ "nav.help": "Help", "nav.logout": "Log out", "nav.dashboard": "Dashboard", + "nav.dashboardsList": "Dashboards", "nav.missions": "Missions", "nav.maps": "Maps & layout", "nav.monitoring-log": "System log", @@ -116,6 +117,40 @@ "dashboard.title": "Dashboard", "dashboard.subtitle": "Widget mission — chạy, xếp hàng và tạm dừng giống MiR Fleet.", + "dashboard.list.title": "Dashboards", + "dashboard.list.subtitle": "Tạo và chỉnh sửa dashboard cho robot.", + "dashboard.list.create": "+ Tạo dashboard", + "dashboard.list.clearFilters": "Xóa bộ lọc", + "dashboard.list.filterLabel": "Lọc:", + "dashboard.list.filterPlaceholder": "Nhập tên để lọc…", + "dashboard.list.itemsFound": "{count} mục", + "dashboard.list.pageOf": "Trang {page} / {total}", + "dashboard.list.col.name": "Tên", + "dashboard.list.col.createdBy": "Tạo bởi", + "dashboard.list.col.functions": "Chức năng", + "dashboard.list.empty": "Không có dashboard nào.", + "dashboard.list.back": "← Danh sách", + "dashboard.list.design": "Thiết kế", + "dashboard.list.active": "Dashboard đang active", + "dashboard.list.edit": "Sửa", + "dashboard.list.delete": "Xóa", + "dashboard.list.deleteConfirm": "Xóa dashboard «{name}»?", + "dashboard.list.cannotDeleteDefault": "Không thể xóa Default Dashboard.", + "dashboard.list.noEditPermission": "Bạn không có quyền chỉnh sửa dashboard này.", + "dashboard.dialog.create.title": "Tạo dashboard", + "dashboard.create.title": "Tạo dashboard", + "dashboard.create.subtitle": "Tạo dashboard mới trên robot.", + "dashboard.create.backToList": "← Quay lại danh sách", + "dashboard.create.name": "Tên", + "dashboard.create.namePlaceholder": "VD: John's Dashboard", + "dashboard.create.permissions": "Chọn user groups được phép chỉnh sửa dashboard này.", + "dashboard.create.permissionsBtn": "Quyền", + "dashboard.create.permissionsTitle": "Quyền chỉnh sửa", + "dashboard.create.submit": "Tạo dashboard", + "dashboard.create.cancel": "Hủy", + "dashboard.dialog.editDashboard.title": "Sửa dashboard", + "dashboard.designer.empty": "Chưa có widget. Phase B sẽ thêm designer đầy đủ.", + "dashboard.createdBy.system": "MiR", "dashboard.addWidget": "Thêm widget", "dashboard.editLayout": "Sửa layout", "dashboard.editDone": "Xong", @@ -341,7 +376,7 @@ "help.api.body2": "Reference Guide MiR rev 1.9: docs/Reference guide.pdf", }, en: { - "app.title": "LiDAR Manager", + "app.title": "Robot App", "app.robotName": "RobotApp", "app.status.ready": "Ready", "app.status.reloaded": "Reloaded", @@ -401,6 +436,7 @@ "nav.help": "Help", "nav.logout": "Log out", "nav.dashboard": "Dashboard", + "nav.dashboardsList": "Dashboards", "nav.missions": "Missions", "nav.maps": "Maps & layout", "nav.monitoring-log": "System log", @@ -451,6 +487,40 @@ "dashboard.title": "Dashboard", "dashboard.subtitle": "Mission widgets — run, queue and pause like MiR Fleet.", + "dashboard.list.title": "Dashboards", + "dashboard.list.subtitle": "Create and edit dashboards for the robot.", + "dashboard.list.create": "+ Create dashboard", + "dashboard.list.clearFilters": "Clear filters", + "dashboard.list.filterLabel": "Filter:", + "dashboard.list.filterPlaceholder": "Write name to filter by…", + "dashboard.list.itemsFound": "{count} item(s) found", + "dashboard.list.pageOf": "Page {page} of {total}", + "dashboard.list.col.name": "Name", + "dashboard.list.col.createdBy": "Created by", + "dashboard.list.col.functions": "Functions", + "dashboard.list.empty": "No dashboards found.", + "dashboard.list.back": "← Back to list", + "dashboard.list.design": "Design", + "dashboard.list.active": "Active dashboard", + "dashboard.list.edit": "Edit", + "dashboard.list.delete": "Delete", + "dashboard.list.deleteConfirm": "Delete dashboard «{name}»?", + "dashboard.list.cannotDeleteDefault": "Cannot delete Default Dashboard.", + "dashboard.list.noEditPermission": "You do not have permission to edit this dashboard.", + "dashboard.dialog.create.title": "Create dashboard", + "dashboard.create.title": "Create dashboard", + "dashboard.create.subtitle": "Create a new dashboard in the robot.", + "dashboard.create.backToList": "← Back to the list", + "dashboard.create.name": "Name", + "dashboard.create.namePlaceholder": "John's Dashboard", + "dashboard.create.permissions": "Select user groups allowed to edit this dashboard.", + "dashboard.create.permissionsBtn": "Permissions", + "dashboard.create.permissionsTitle": "Permissions", + "dashboard.create.submit": "Create dashboard", + "dashboard.create.cancel": "Cancel", + "dashboard.dialog.editDashboard.title": "Edit dashboard", + "dashboard.designer.empty": "No widgets yet. Full designer coming in Phase B.", + "dashboard.createdBy.system": "MiR", "dashboard.addWidget": "Add widget", "dashboard.editLayout": "Edit layout", "dashboard.editDone": "Done", diff --git a/www/index.html b/www/index.html index e41284c..ba121df 100644 --- a/www/index.html +++ b/www/index.html @@ -3,7 +3,7 @@ -+ Tạo và chỉnh sửa dashboard cho robot. + +
+Chưa có widget. Bấm «Thêm widget» để bắt đầu.
-+ Tạo dashboard mới trên robot. + +
+Chưa có widget. Phase B sẽ thêm designer đầy đủ.