diff --git a/storybook/PagesModel.qml b/storybook/PagesModel.qml index d4cfeb0738..3cd4da9cc2 100644 --- a/storybook/PagesModel.qml +++ b/storybook/PagesModel.qml @@ -201,6 +201,10 @@ ListModel { title: "StatusInfoBoxPanel" section: "Panels" } + ListElement { + title: "OverviewSettingsChart" + section: "Panels" + } ListElement { title: "BurnTokensPopup" section: "Popups" diff --git a/storybook/figma.json b/storybook/figma.json index b5b395b304..175d4f724e 100644 --- a/storybook/figma.json +++ b/storybook/figma.json @@ -236,5 +236,8 @@ ], "OwnerTokenWelcomeView": [ "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?type=design&node-id=34794%3A590064&mode=design&t=eabTmd6JZbuycoy8-1" + ], + "OverviewSettingsChart": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba⎜Desktop?type=design&node-id=31281-635619&mode=design&t=RYpVRgwqCjp8fUEX-0" ] } diff --git a/storybook/pages/OverviewSettingsChartPage.qml b/storybook/pages/OverviewSettingsChartPage.qml new file mode 100644 index 0000000000..edc89f5338 --- /dev/null +++ b/storybook/pages/OverviewSettingsChartPage.qml @@ -0,0 +1,30 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import AppLayouts.Communities.panels 1.0 +import Models 1.0 + +SplitView { + + OverviewSettingsChart { + id: chart + SplitView.fillWidth: true + SplitView.fillHeight: true + + model: generateRandomModel() + } + + function generateRandomModel() { + var newModel = [] + const now = Date.now() + for(var i = 0; i < 500000; i++) { + var date = generateRandomDate(1463154962000, now) + newModel.push(date) + } + return newModel + } + + function generateRandomDate(from, to) { + return from + Math.random() * (to - from) + } +} diff --git a/ui/StatusQ/src/StatusQ/Components/StatusChartPanel.qml b/ui/StatusQ/src/StatusQ/Components/StatusChartPanel.qml index 110253ca82..c5e69997ff 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusChartPanel.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusChartPanel.qml @@ -97,6 +97,18 @@ Page { */ property int defaultTimeRangeIndexShown: 0 + /*! + \qmlproperty int StatusChartPanel::headerLeftPadding + This property holds the left padding of the header. + */ + property int headerLeftPadding: 46 + + /*! + \qmlproperty int StatusChartPanel::headerBottomPadding + This property holds the bottom padding of the header. + */ + property int headerBottomPadding: 0 + /*! \qmlsignal This signal is emitted when a header tab bar is clicked. @@ -141,10 +153,10 @@ Page { background: null header: Item { - height: childrenRect.height + height: childrenRect.height + root.headerBottomPadding RowLayout { anchors.left: parent.left - anchors.leftMargin: 46 + anchors.leftMargin: root.headerLeftPadding anchors.right: parent.right StatusTabBar { id: graphsTabBar diff --git a/ui/StatusQ/src/StatusQ/Components/private/chart/Chart.js b/ui/StatusQ/src/StatusQ/Components/private/chart/Chart.js index 050f722e9b..48bf00a588 100644 --- a/ui/StatusQ/src/StatusQ/Components/private/chart/Chart.js +++ b/ui/StatusQ/src/StatusQ/Components/private/chart/Chart.js @@ -10239,7 +10239,7 @@ helpers$1.extend(Chart.prototype, /** @lends Chart */ { // Invoke onHover hook // Need to call with native event here to not break backwards compatibility - helpers$1.callback(options.onHover || options.hover.onHover, [e.native, me.active], me); + helpers$1.callback(options.onHover || options.hover.onHover, [e.native, me.active, e], me); if (e.type === 'mouseup' || e.type === 'click') { if (options.onClick) { diff --git a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml index 4bd744e459..0762f03039 100644 --- a/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml +++ b/ui/StatusQ/src/StatusQ/Core/LocaleUtils.qml @@ -248,6 +248,101 @@ QtObject { return Math.round((d1 - d2) / d.msInADay) // Math.round: not all days are 24 hours long! } + /** + * Returns the timestamp of the last minute + * - `before``: number of minutes before the reference minute + * - `time``: timestamp to use as reference + * - `rounding``: if true, rounds to the last minute + **/ + function minutes(before = 0, time = Date.now(), rounding = true) { + let timestamp = rounding ? Math.floor(time / minutesToMs(5)) * minutesToMs(5) + : time + return timestamp - minutesToMs(before) + } + + /** + * Returns the timestamp of the last hour + * - `before``: number of hours before the reference hour + * - `time``: timestamp to use as reference + * - `rounding``: if true, rounds to the last hour + **/ + function hours(before = 0, time = Date.now(), rounding = true) { + let timestamp = rounding ? Math.floor(time / minutesToMs(30)) * minutesToMs(30) + : time + return timestamp - hoursToMs(before) + } + + /** + * Returns the timestamp of the last day + * - `before``: number of days before the reference day + * - `time``: timestamp to use as reference + * - `rounding``: if true, rounds to the last day + **/ + function days(before = 0, time = Date.now(), rounding = true) { + let date = new Date(time) + if(rounding) { + date = new Date(date.getFullYear(), date.getMonth(), date.getDate()) + } + + date.setDate(date.getDate() - before) + return date.getTime() + } + + /** + * Returns the timestamp of the last week + * - `before``: number of weeks before the reference week + * - `time``: timestamp to use as reference + * - `rounding``: if true, rounds to the last week + **/ + function months(before = 0, time = Date.now(), rounding = true) { + let date = new Date(time) + if(rounding) { + date = new Date(date.getFullYear(), date.getMonth(), 1) + } + date.setMonth(date.getMonth() - before) + + return date.getTime() + } + + /** + * Returns the timestamp of the last year + * - `before``: number of years before the reference year + * - `time``: timestamp to use as reference + * - `rounding``: if true, rounds to the last year + **/ + function years(before = 0, time = Date.now(), rounding = true) { + let date = new Date(time) + if(rounding) { + date = new Date(date.getFullYear(), 0, 1) + } + date.setFullYear(date.getFullYear() - before) + return date.getTime() + } + + /** + * Retuns the number of milliseconds in the given amount of minutes + * - `count``: number of minutes + **/ + function minutesToMs(count = 1) { + return count * 60 * 1000 + } + + /** + * Retuns the number of milliseconds in the given amount of hours + * - `count``: number of hours + **/ + function hoursToMs(count = 1) { + return count * minutesToMs(60) + } + + /** + * Retuns the number of milliseconds in the given amount of days + * - `count``: number of days + **/ + function daysToMs(count = 1) { + return count * hoursToMs(24) + } + /** Converts the Date to a string containing the date suitable for the specified locale in the specified format. diff --git a/ui/app/AppLayouts/Communities/panels/OverviewSettingsChart.qml b/ui/app/AppLayouts/Communities/panels/OverviewSettingsChart.qml new file mode 100644 index 0000000000..3fdee7a843 --- /dev/null +++ b/ui/app/AppLayouts/Communities/panels/OverviewSettingsChart.qml @@ -0,0 +1,371 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml 2.15 + +import StatusQ.Popups 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 + +import utils 1.0 + +StatusChartPanel { + id: root + /** + * Flat model to use for the chart containing timestamps + * type: {Array} + */ + property var model: [] + + QtObject { + id: d + + //visual properties + readonly property string baseColor1: Theme.palette.baseColor1 + readonly property string twentyPercentBaseColor1: Theme.palette.alphaColor(baseColor1, 0.2) + readonly property string barColor: Theme.palette.primaryColor2 + readonly property string barBorderColor: Theme.palette.primaryColor1 + readonly property string messagesLabel: qsTr("Messages") + + property int hoveredBarIndex: 0 + property int hoveredBarValue: 0 + readonly property var hoveredModelMetadata: modelMetadata[root.timeRangeTabBarIndex].modelItems[hoveredBarIndex] + readonly property var tooltipConfig: modelMetadata[root.timeRangeTabBarIndex].tooltipConfig + readonly property var graphTabsModel: [{text: messagesLabel, enabled: true}] + readonly property var now: Date.now() + + readonly property var chartData: selectedTabInfo.modelItems.map(x => d.itemsCountInRange(root.model, x.start, x.end)) + readonly property var labels: selectedTabInfo.modelItems.map(x => x.label) + readonly property var selectedTabInfo: modelMetadata[root.timeRangeTabBarIndex] + readonly property var modelMetadata: [ + { + text: qsTr("1H"), + modelItems: [ + { start: LocaleUtils.minutes(60, now), end: LocaleUtils.minutes(50, now), label: minutesStr(55)}, + { start: LocaleUtils.minutes(50, now), end: LocaleUtils.minutes(40, now), label: minutesStr(45)}, + { start: LocaleUtils.minutes(40, now), end: LocaleUtils.minutes(30, now), label: minutesStr(35)}, + { start: LocaleUtils.minutes(30, now), end: LocaleUtils.minutes(20, now), label: minutesStr(25)}, + { start: LocaleUtils.minutes(20, now), end: LocaleUtils.minutes(10, now), label: minutesStr(15)}, + { start: LocaleUtils.minutes(10, now), end: LocaleUtils.minutes(0, now, false), label: minutesStr(5)} + ], + tooltipConfig: { + timeRangeString: qsTr("Time period"), + timeRangeFormatter: d.hoursRangeStr + }, + }, + { + text: qsTr("1D"), + modelItems: [ + { start: LocaleUtils.hours(24, now), end: LocaleUtils.hours(20, now), label: hourStr(22)}, + { start: LocaleUtils.hours(20, now), end: LocaleUtils.hours(16, now), label: hourStr(18)}, + { start: LocaleUtils.hours(16, now), end: LocaleUtils.hours(12, now), label: hourStr(14)}, + { start: LocaleUtils.hours(12, now), end: LocaleUtils.hours(8, now), label: hourStr(10)}, + { start: LocaleUtils.hours(8, now), end: LocaleUtils.hours(4, now), label: hourStr(6)}, + { start: LocaleUtils.hours(4, now), end: LocaleUtils.hours(0, now, false), label: hourStr(2)} + ], + tooltipConfig: { + timeRangeString: qsTr("Time period"), + timeRangeFormatter: d.hoursRangeStr + }, + }, + { + text: qsTr("7D"), + modelItems: [ + { start: LocaleUtils.days(6, now), end: LocaleUtils.days(5, now), label: dayStr(6)}, + { start: LocaleUtils.days(5, now), end: LocaleUtils.days(4, now), label: dayStr(5)}, + { start: LocaleUtils.days(4, now), end: LocaleUtils.days(3, now), label: dayStr(4)}, + { start: LocaleUtils.days(3, now), end: LocaleUtils.days(2, now), label: dayStr(3)}, + { start: LocaleUtils.days(2, now), end: LocaleUtils.days(1, now), label: dayStr(2)}, + { start: LocaleUtils.days(1, now), end: LocaleUtils.days(0, now), label: dayStr(1)}, + { start: LocaleUtils.days(0, now), end: LocaleUtils.days(0, now, false), label: dayStr(0)} + ], + tooltipConfig: { + timeRangeString: qsTr("Date"), + timeRangeFormatter: d.daysRangeStr + }, + }, + { + text: qsTr("1M"), + modelItems: [ + { start: LocaleUtils.days(30, now), end: LocaleUtils.days(25, now), label: dayStr(30)}, + { start: LocaleUtils.days(25, now), end: LocaleUtils.days(20, now), label: dayStr(25)}, + { start: LocaleUtils.days(20, now), end: LocaleUtils.days(15, now), label: dayStr(20)}, + { start: LocaleUtils.days(15, now), end: LocaleUtils.days(10, now), label: dayStr(15)}, + { start: LocaleUtils.days(10, now), end: LocaleUtils.days(5, now), label: dayStr(10)}, + { start: LocaleUtils.days(5, now), end: LocaleUtils.days(0, now, false), label: dayStr(5)} + ], + tooltipConfig: { + timeRangeString: qsTr("Time period"), + timeRangeFormatter: d.daysRangeStr + }, + }, + { + text: qsTr("6M"), + modelItems: [ + { start: LocaleUtils.months(5, now), end: LocaleUtils.months(4, now), label: monthStr(5)}, + { start: LocaleUtils.months(4, now), end: LocaleUtils.months(3, now), label: monthStr(4)}, + { start: LocaleUtils.months(3, now), end: LocaleUtils.months(2, now), label: monthStr(3)}, + { start: LocaleUtils.months(2, now), end: LocaleUtils.months(1, now), label: monthStr(2)}, + { start: LocaleUtils.months(1, now), end: LocaleUtils.months(0, now), label: monthStr(1)}, + { start: LocaleUtils.months(0, now), end: LocaleUtils.months(0, now, false), label: monthStr(0)} + ], + tooltipConfig: { + timeRangeString: qsTr("Month"), + timeRangeFormatter: d.monthsRangeStr + }, + }, + { + text: qsTr("1Y"), + modelItems: [ + { start: LocaleUtils.months(12, now), end: LocaleUtils.months(10, now), label: monthStr(11)}, + { start: LocaleUtils.months(10, now), end: LocaleUtils.months(8, now), label: monthStr(9)}, + { start: LocaleUtils.months(8, now), end: LocaleUtils.months(6, now), label: monthStr(7)}, + { start: LocaleUtils.months(6, now), end: LocaleUtils.months(4, now), label: monthStr(5)}, + { start: LocaleUtils.months(4, now), end: LocaleUtils.months(2, now), label: monthStr(3)}, + { start: LocaleUtils.months(2, now), end: LocaleUtils.months(0, now, false), label: monthStr(1)} + ], + tooltipConfig: { + timeRangeString: qsTr("Time period"), + timeRangeFormatter: d.monthsRangeStr + }, + }, + { + text: qsTr("ALL"), + modelItems: [ + { start: LocaleUtils.years(7, now), end: LocaleUtils.years(6, now), label: yearsStr(7) }, + { start: LocaleUtils.years(6, now), end: LocaleUtils.years(5, now), label: yearsStr(6) }, + { start: LocaleUtils.years(5, now), end: LocaleUtils.years(4, now), label: yearsStr(5) }, + { start: LocaleUtils.years(4, now), end: LocaleUtils.years(3, now), label: yearsStr(4) }, + { start: LocaleUtils.years(3, now), end: LocaleUtils.years(2, now), label: yearsStr(3) }, + { start: LocaleUtils.years(2, now), end: LocaleUtils.years(1, now), label: yearsStr(2) }, + { start: LocaleUtils.years(1, now), end: LocaleUtils.years(0, now), label: yearsStr(1) }, + { start: LocaleUtils.years(0, now), end: LocaleUtils.years(0, now, false), label: yearsStr(0) } + ], + tooltipConfig: { + timeRangeString: qsTr("Year"), + timeRangeFormatter: d.yearsRangeStr + }, + } + ] + + function itemsCountInRange(array, start, end) { + return array ? array.filter(x => x <= end && x > start).length : 0 + } + + function minutesStr(before = 0, timeReference = now, roundCurrentTime = true) { + return LocaleUtils.formatTime(LocaleUtils.minutes(before, timeReference, roundCurrentTime), Locale.ShortFormat) + } + + function hourStr(before = 0, timeReference = now, roundCurrentTime = true) { + return LocaleUtils.formatTime(LocaleUtils.hours(before, timeReference, roundCurrentTime), Locale.ShortFormat) + } + + function dayStr(before = 0, timeReference = now, roundCurrentTime = true) { + return LocaleUtils.getDayMonth(LocaleUtils.days(before, timeReference, roundCurrentTime), Locale.ShortFormat) + } + + function monthStr(before = 0, timeReference = now, roundCurrentTime = true, shortFormat = true) { + const format = shortFormat ? "MMM" : "MMMM" + const timeStamp = LocaleUtils.months(before, timeReference, roundCurrentTime) + return LocaleUtils.formatDate(timeStamp, format) + } + + function yearsStr(before = 0, timeReference = now, roundCurrentTime = true) { + return LocaleUtils.formatDate(LocaleUtils.years(before, timeReference, roundCurrentTime), "yyyy"); + } + + function hoursRangeStr(start, end) { + return "%1 - %2".arg(hourStr(0, start, false)).arg(hourStr(0, end, false)) + } + + function daysRangeStr(start, end) { + return (end - start > LocaleUtils.daysToMs(1)) ? + "%1 - %2".arg(dayStr(0, start, false)).arg(dayStr(0, end, false)) : + dayStr(0, start, false) + } + + function monthsRangeStr(start, end) { + //End date excluded + //Adjust by one ms to exclude the end date + //To avoid considering the end date as a new month + end = end - 1 + const startDate = monthStr(0, start, false) + const endDate = monthStr(0, end, false) + return (startDate !== endDate) ? + "%1 - %2".arg(startDate).arg(endDate) : + monthStr(0, start, false, false) + } + + function yearsRangeStr(start, end) { + //End date excluded + //Adjust by one ms to exclude the end date + //To avoid considering the end date as a new year + end = end - 1 + const startYear = yearsStr(0, start, false) + const endYear = yearsStr(0, end, false) + return (startYear !== endYear) ? + "%1 - %2".arg(startYear).arg(endYear) : + startYear + } + + function getAdjustedTooltipPosition(event) { + // By defaullt the popup is displayed on the right of the cursor + // If there is not enough space on the right, display it on the left + const relativeMousePoint = event.target.mapToItem(toolTip.parent, event.x, event.y) // relative to tooltip parent + const leftPositon = (toolTip.parent.width - (toolTip.width + toolTip.rightPadding + relativeMousePoint.x + 15)) < 0 + return leftPositon ? Qt.point(relativeMousePoint.x - toolTip.width - 15, relativeMousePoint.y - 5) + : Qt.point(relativeMousePoint.x + 15, relativeMousePoint.y - 5) + } + } + headerLeftPadding: 0 + headerBottomPadding: Style.current.bigPadding + graphsModel: d.graphTabsModel + timeRangeModel: d.modelMetadata + onHeaderTabClicked: { + root.chart.animateToNewData(); + } + + ///////////////////////////// + // Chartjs configuration // + ///////////////////////////// + chart.chartType: 'bar' + chart.chartData: { + return { + labels: d.labels, + datasets: [{ + xAxisId: 'x-axis-1', + yAxisId: 'y-axis-1', + backgroundColor: d.barColor, + pointRadius: 0, + hoverBackgroundColor: d.barColor, + hoverBorderColor: d.barBorderColor, + hoverBorderWidth: 2, + data: d.chartData + }] + } + } + + chart.chartOptions: { + return { + maintainAspectRatio: false, + responsive: true, + legend: { + display: false + }, + // Popup follows the cursor + onHover: function(arg1, hoveredItems, event) { + if(!event || hoveredItems.length == 0) { + toolTip.close() + return + } + + d.hoveredBarIndex = hoveredItems[0]._index + d.hoveredBarValue = hoveredItems[0]._chart.config.data.datasets[0].data[hoveredItems[0]._index] + const position = d.getAdjustedTooltipPosition(event) + toolTip.popup(position.x, position.y) + }, + tooltips: { + enabled: false, + }, + scales: { + xAxes: [{ + id: 'x-axis-1', + position: 'bottom', + stacked: false, + gridLines: { + drawOnChartArea: false, + drawBorder: false, + drawTicks: false, + }, + ticks: { + fontSize: Style.current.asideTextFontSize, + fontColor: d.baseColor1, + padding: Style.current.padding, + } + }], + yAxes: [{ + position: 'left', + id: 'y-axis-1', + stacked: false, + gridLines: { + borderDash: [5, 3], + lineWidth: 1, + drawBorder: false, + drawTicks: false, + color: d.twentyPercentBaseColor1, + }, + beforeDataLimits: (axis) => { + axis.paddingTop = 25; + axis.paddingBottom = 0; + }, + ticks: { + fontSize: 10, + fontColor: d.baseColor1, + padding: Style.current.halfPadding, + maxTicksLimit: Style.current.asideTextFontSize, + beginAtZero: true, + stepSize: 1, + callback: function(value, index, values) { + return LocaleUtils.numberToLocaleString(value) + } + } + }] + } + } + } + + StatusMenu { + id: toolTip + width: 243 //By design + topPadding: Style.current.padding + bottomPadding: topPadding + leftPadding: topPadding + rightPadding: topPadding + parent: Overlay.overlay + + ColumnLayout { + spacing: Style.current.padding + RowLayout { + Layout.fillWidth: true + StatusBaseText { + elide: Qt.ElideRight + font.pixelSize: Style.current.primaryTextFontSize + color: Theme.palette.baseColor1 + text: d.tooltipConfig.timeRangeString + } + Item { + Layout.fillWidth: true + } + StatusBaseText { + Layout.alignment: Qt.AlignRight + elide: Qt.ElideRight + font.pixelSize: Style.current.primaryTextFontSize + color: Theme.palette.directColor1 + text: d.hoveredModelMetadata ? d.tooltipConfig.timeRangeFormatter(d.hoveredModelMetadata.start, d.hoveredModelMetadata.end) + : "" + } + } + + RowLayout { + Layout.fillWidth: true + StatusBaseText { + elide: Qt.ElideRight + font.pixelSize: Style.current.primaryTextFontSize + color: Theme.palette.baseColor1 + text: qsTr("No. of Messages") + } + Item { Layout.fillWidth: true } + StatusBaseText { + Layout.alignment: Qt.AlignRight + elide: Qt.ElideRight + font.pixelSize: Style.current.primaryTextFontSize + color: Theme.palette.directColor1 + text: LocaleUtils.numberToLocaleString(d.hoveredBarValue) + } + } + } + } +} diff --git a/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml b/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml index cf55c92244..4633d955bb 100644 --- a/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/OverviewSettingsPanel.qml @@ -55,7 +55,7 @@ StackLayout { SettingsPage { rightPadding: 64 - bottomPadding: 64 + bottomPadding: 50 topPadding: 0 header: null contentItem: ColumnLayout { @@ -109,38 +109,28 @@ StackLayout { Layout.fillWidth: true implicitHeight: 1 - visible: root.editable color: Theme.palette.statusMenu.separatorColor } - RowLayout { + OverviewSettingsChart { + Layout.topMargin: 16 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.bottomMargin: 16 + } + Rectangle { Layout.fillWidth: true - visible: root.owned - - StatusIcon { - icon: "info" - color: Theme.palette.directColor1 - } - - StatusBaseText { - Layout.fillWidth: true - text: qsTr("This node is the Community Owner Node. For your Community to function correctly try to keep this computer with Status running and online as much as possible.") - font.pixelSize: 15 - color: Theme.palette.directColor1 - wrapMode: Text.WordWrap - } - } - - Item { - Layout.fillHeight: true + implicitHeight: 1 + color: Theme.palette.statusMenu.separatorColor } } footer: OverviewSettingsFooter { rightPadding: 64 leftPadding: 64 - bottomPadding: 50 + bottomPadding: 64 + topPadding: 0 loginType: root.loginType communityName: root.name //TODO connect to backend diff --git a/ui/app/AppLayouts/Communities/panels/qmldir b/ui/app/AppLayouts/Communities/panels/qmldir index 97101da706..fb9e10ea8c 100644 --- a/ui/app/AppLayouts/Communities/panels/qmldir +++ b/ui/app/AppLayouts/Communities/panels/qmldir @@ -30,4 +30,5 @@ TokenHoldersPanel 1.0 TokenHoldersPanel.qml TokenHoldersProxyModel 1.0 TokenHoldersProxyModel.qml WarningPanel 1.0 WarningPanel.qml WelcomeBannerPanel 1.0 WelcomeBannerPanel.qml -EditSettingsPanel 1.0 EditSettingsPanel.qml \ No newline at end of file +EditSettingsPanel 1.0 EditSettingsPanel.qml +OverviewSettingsChart 1.0 OverviewSettingsChart.qml \ No newline at end of file