@things-factory/dataset 5.0.14 → 5.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +40 -16
  2. package/assets/data-report.jpg +0 -0
  3. package/assets/diagram.jpg +0 -0
  4. package/assets/glue-table-indices.png +0 -0
  5. package/client/pages/data-archive/data-archive-list-page.js +290 -0
  6. package/client/pages/data-archive/data-archive-request-popup.js +180 -0
  7. package/client/pages/data-key-set/data-key-set-list-page.js +98 -3
  8. package/client/pages/data-report/data-report-embed-page.js +16 -12
  9. package/client/pages/data-report/data-report-list-page.js +18 -15
  10. package/client/pages/data-report/data-report-samples-page.js +186 -0
  11. package/client/pages/data-set/data-set-list-page.js +9 -1
  12. package/client/route.js +20 -12
  13. package/config/config.development.js +21 -0
  14. package/config/config.production.js +32 -11
  15. package/dist-server/controllers/create-data-sample.js +3 -1
  16. package/dist-server/controllers/create-data-sample.js.map +1 -1
  17. package/dist-server/controllers/jasper-report.js +19 -14
  18. package/dist-server/controllers/jasper-report.js.map +1 -1
  19. package/dist-server/controllers/shiny-report.js +41 -0
  20. package/dist-server/controllers/shiny-report.js.map +1 -0
  21. package/dist-server/routes.js +8 -0
  22. package/dist-server/routes.js.map +1 -1
  23. package/dist-server/service/data-archive/data-archive-mutation.js +227 -0
  24. package/dist-server/service/data-archive/data-archive-mutation.js.map +1 -0
  25. package/dist-server/service/data-archive/data-archive-query.js +85 -0
  26. package/dist-server/service/data-archive/data-archive-query.js.map +1 -0
  27. package/dist-server/service/data-archive/data-archive-type.js +74 -0
  28. package/dist-server/service/data-archive/data-archive-type.js.map +1 -0
  29. package/dist-server/service/data-archive/data-archive.js +80 -0
  30. package/dist-server/service/data-archive/data-archive.js.map +1 -0
  31. package/dist-server/service/data-archive/index.js +9 -0
  32. package/dist-server/service/data-archive/index.js.map +1 -0
  33. package/dist-server/service/data-key-set/data-key-item-type.js.map +1 -1
  34. package/dist-server/service/data-key-set/data-key-set-mutation.js.map +1 -1
  35. package/dist-server/service/data-key-set/data-key-set-query.js.map +1 -1
  36. package/dist-server/service/data-key-set/data-key-set-type.js +17 -0
  37. package/dist-server/service/data-key-set/data-key-set-type.js.map +1 -1
  38. package/dist-server/service/data-key-set/data-key-set.js +11 -0
  39. package/dist-server/service/data-key-set/data-key-set.js.map +1 -1
  40. package/dist-server/service/data-ooc/data-ooc-mutation.js.map +1 -1
  41. package/dist-server/service/data-ooc/data-ooc-query.js.map +1 -1
  42. package/dist-server/service/data-ooc/data-ooc-subscription.js.map +1 -1
  43. package/dist-server/service/data-ooc/data-ooc-type.js.map +1 -1
  44. package/dist-server/service/data-ooc/data-ooc.js.map +1 -1
  45. package/dist-server/service/data-sample/data-sample-mutation.js.map +1 -1
  46. package/dist-server/service/data-sample/data-sample-query.js.map +1 -1
  47. package/dist-server/service/data-sample/data-sample-type.js.map +1 -1
  48. package/dist-server/service/data-sample/data-sample.js.map +1 -1
  49. package/dist-server/service/data-sensor/data-sensor-mutation.js.map +1 -1
  50. package/dist-server/service/data-sensor/data-sensor-query.js.map +1 -1
  51. package/dist-server/service/data-sensor/data-sensor-type.js.map +1 -1
  52. package/dist-server/service/data-sensor/data-sensor.js.map +1 -1
  53. package/dist-server/service/data-set/data-item-type.js.map +1 -1
  54. package/dist-server/service/data-set/data-set-mutation.js.map +1 -1
  55. package/dist-server/service/data-set/data-set-query.js.map +1 -1
  56. package/dist-server/service/data-set/data-set-type.js.map +1 -1
  57. package/dist-server/service/data-set/data-set.js +2 -0
  58. package/dist-server/service/data-set/data-set.js.map +1 -1
  59. package/dist-server/service/data-set-history/data-set-history-query.js.map +1 -1
  60. package/dist-server/service/data-set-history/data-set-history-type.js.map +1 -1
  61. package/dist-server/service/data-set-history/data-set-history.js.map +1 -1
  62. package/dist-server/service/data-set-history/event-subscriber.js.map +1 -1
  63. package/dist-server/service/data-spec/data-spec-query.js.map +1 -1
  64. package/dist-server/service/data-spec/data-spec.js.map +1 -1
  65. package/dist-server/service/index.js +5 -2
  66. package/dist-server/service/index.js.map +1 -1
  67. package/dist-server/tsconfig.tsbuildinfo +1 -1
  68. package/dist-server/utils/config-resolver.js +33 -0
  69. package/dist-server/utils/config-resolver.js.map +1 -0
  70. package/dist-server/utils/index.js +5 -0
  71. package/dist-server/utils/index.js.map +1 -0
  72. package/helps/dataset/data-archive.md +9 -0
  73. package/helps/dataset/data-entry-list.md +2 -0
  74. package/helps/dataset/data-key-set.md +10 -0
  75. package/helps/dataset/data-ooc.md +11 -0
  76. package/helps/dataset/data-report-list.md +4 -0
  77. package/helps/dataset/data-sample-search.md +2 -0
  78. package/helps/dataset/data-sample.md +11 -0
  79. package/helps/dataset/data-sensor.md +13 -0
  80. package/helps/dataset/data-set.md +28 -0
  81. package/helps/dataset/ui/data-item-list.md +19 -0
  82. package/helps/dataset/ui/data-key-item-list.md +2 -0
  83. package/package.json +7 -7
  84. package/server/controllers/create-data-sample.ts +4 -0
  85. package/server/controllers/jasper-report.ts +20 -14
  86. package/server/controllers/shiny-report.ts +60 -0
  87. package/server/routes.ts +11 -0
  88. package/server/service/data-archive/data-archive-mutation.ts +253 -0
  89. package/server/service/data-archive/data-archive-query.ts +56 -0
  90. package/server/service/data-archive/data-archive-type.ts +49 -0
  91. package/server/service/data-archive/data-archive.ts +69 -0
  92. package/server/service/data-archive/index.ts +6 -0
  93. package/server/service/data-key-set/data-key-set-type.ts +13 -0
  94. package/server/service/data-key-set/data-key-set.ts +9 -0
  95. package/server/service/data-set/data-set.ts +3 -1
  96. package/server/service/index.ts +5 -2
  97. package/server/utils/config-resolver.ts +29 -0
  98. package/server/utils/index.ts +1 -0
  99. package/things-factory.config.js +20 -12
  100. package/translations/en.json +18 -2
  101. package/translations/ko.json +16 -1
  102. package/translations/ms.json +2 -0
  103. package/translations/zh.json +2 -0
  104. package/dist-server/service/data-item/data-item-mutation.js +0 -69
  105. package/dist-server/service/data-item/data-item-mutation.js.map +0 -1
  106. package/dist-server/service/data-item/data-item-query.js +0 -100
  107. package/dist-server/service/data-item/data-item-query.js.map +0 -1
  108. package/dist-server/service/data-item/data-item-type.js +0 -80
  109. package/dist-server/service/data-item/data-item-type.js.map +0 -1
  110. package/dist-server/service/data-item/data-item.js +0 -136
  111. package/dist-server/service/data-item/data-item.js.map +0 -1
  112. package/dist-server/service/data-item/index.js +0 -9
  113. package/dist-server/service/data-item/index.js.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # dataset
2
- ## Architecture for data-samples
2
+ ## dataset relation diagram
3
+ ![ERD Diagram](./assets/diagram.jpg)
4
+ - data_sets 하나가 여러 data_items 를 갖습니다. data_sets와 data_items 조합으로 CCP 항목들을 정의합니다.
5
+ - data_sets는 또한 data_key_sets를 참고할 수 있습니다. data_key_sets에서는 data_samples, data_oocs의 조회 인덱스를 위한 key(key_01~key_05)를 정의합니다. data_samples, data_oocs 데이타 생성 시점에, data 컬럼 값으로 부터 별도 컬럼(key_##)에 해당 값을 저장합니다.
6
+ - data_set_histories에는 data_sets의 변경 내역이 추가됩니다. data_sets의 id는 data_set_histories에 original_id로 추가됩니다. data_set_histories를 사용하려면 original_id 와 version이 필요합니다.
7
+ - data_sensors에는 자동 수집되는 디바이스 정보를 정의합니다. data_sets를 참고하여 CCP를 관리하도록 합니다.
8
+ - data_samples에는 입력받은 실제 데이터가 저장됩니다.
9
+ - data_oocs에는 data_sets의 CCP 스펙을 벗어나는 경우, data_samples의 데이터를 복사하여 추가됩니다.
10
+ - data_archives에는 다운로드 받기 위한 요청 정보 및 상태가 저장됩니다.
11
+
12
+ ## Architecture for collecting data samples
3
13
  ![Architecture for data-samples](./assets/data-samples.jpg)
14
+ data_samples, data_oocs, data_set_histories의 테이블 변경 사항은 Database CDC로 Kafka에 전송 및 저장되고 토픽별로 다음의 lambda 들이 트리거되어 S3로 저장합니다. 이 S3 데이터는 Glue Crawler, Glue Data Catalog를 거쳐 Athena에서 조회할 수 있습니다. hive 스타일의 S3 파티션 정보들을 업데이트 하는 작업이 스케줄링되어 작동하고, 파티션 정보가 업데이트 된 S3파일들이 Athena에서 조회되므로 실시간으로 데이터가 반영되지는 않습니다.
15
+ - [func-data-samples-to-s3](https://github.com/operatochef/serverless/tree/main/func-data-samples-to-s3)
16
+ - [func-data-oocs-to-s3](https://github.com/operatochef/serverless/tree/main/func-data-oocs-to-s3)
17
+ - [func-data-set-histories-to-s3](https://github.com/operatochef/serverless/tree/main/func-data-set-histories-to-s3)
18
+
19
+ 개발용으로는 reference-app을 사용하고 있으며, 여기서는 func-dev-* 로 시작하는 lambda들을 사용합니다.
20
+ ## data-report process
21
+ ![data-report process](./assets/data-report.jpg)
22
+
23
+ 'shiny' uses the next lambda. But 'jasper' is not using this lambda now. Consider using the same data service.
24
+ - [func-transform-data-samples](https://github.com/operatochef/serverless/tree/main/func-transform-data-samples)
25
+
4
26
  ## Partition Keys
5
27
  At the early stage, partition keys are desinged for dynamic partitioning for Athena. But It will be required so many AWS Glue crawlers also by each 'data-sets'.
6
28
 
@@ -17,18 +39,20 @@ Now partition keys are fixed
17
39
  - workdate: working date
18
40
  - workshift
19
41
 
20
- ## PostgreSQL query for dataset
21
- ```
22
- select
23
- data::json#>>'{product,0}' as product,
24
- data::json#>>'{operation,0}' as operation
25
- from data_samples ds
26
- where 1=1
27
- and data_set_id = 'f1e394cb-9ace-4686-af14-817c619ae512'
28
- and data_set_version = 3
29
- and data::json#>>'{operation,0}' = '세척'
30
- order by created_at desc;
31
- ```
32
-
33
- Reference.
34
- - [1] https://www.postgresql.org/docs/11/functions-json.html
42
+ ### Glue Catalog Table Indices
43
+ Partitions can be indices. You can create new indices with combination of partitions.
44
+ It affects query performance and finding partitions.
45
+ ![partitions and indices](./assets/glue-table-indices.png)
46
+
47
+ ## Timestamp of Data Samples
48
+ sample data could have an own timestamp if it is from a sensor data.
49
+ 'collected_at' uses this timestamp.
50
+ Manual type of data is used 'Date.now() and new Date()'
51
+ Graphql might affect timezone of server os for Date type field.
52
+ 'TZ=UTC node' can help to solve this when server starts.
53
+ Or use ``` process.env.TZ = 'UTC' ```.
54
+ Now applied in 'create-data-samples.ts'
55
+
56
+ ## Data Archive
57
+ 제출용 데이터를 위해 data_samples를 다운로드 하는 기능입니다. csv.gz 형태로 다운로드 가능한 임시 url을 제공합니다. 해당 기능을 위해 [func-data-set-download](https://github.com/operatochef/serverless/tree/main/func-data-set-download) lambda를 사용합니다. 이는 수동 수집용 데이터에 적합합니다. 자동 수집 데이터는 lambda로 처리하기에는 많은 메모리가 필요할 수 있습니다. 따라서, 데이타셋 유형별 처리가 필요할 것 같습니다.
58
+ Glue Job을 이용한 처리를 진행해보았으나, 시간이 너무 오래걸리는 문제가 있었습니다. 해당 내용은 AWS ibex계정의 Glue Jobs - 'reference_app_samples_archive' 리소스를 참고바랍니다.
Binary file
Binary file
Binary file
@@ -0,0 +1,290 @@
1
+ import '@operato/data-grist'
2
+
3
+ import gql from 'graphql-tag'
4
+ import { css, html } from 'lit'
5
+ import { connect } from 'pwa-helpers/connect-mixin'
6
+
7
+ import { client } from '@operato/graphql'
8
+ import { i18next, localize } from '@operato/i18n'
9
+ import { openPopup } from '@operato/layout'
10
+ import { PageView, store } from '@operato/shell'
11
+ import { CommonGristStyles, ScrollbarStyles } from '@operato/styles'
12
+ import { isMobileDevice } from '@operato/utils'
13
+
14
+ import './data-archive-request-popup'
15
+
16
+ export class DataArchiveListPage extends connect(store)(localize(i18next)(PageView)) {
17
+ static get properties() {
18
+ return {
19
+ active: String,
20
+ gristConfig: Object,
21
+ mode: String
22
+ }
23
+ }
24
+
25
+ static get styles() {
26
+ return [
27
+ ScrollbarStyles,
28
+ CommonGristStyles,
29
+ css`
30
+ :host {
31
+ display: flex;
32
+ flex-direction: column;
33
+
34
+ overflow: hidden;
35
+ }
36
+
37
+ ox-grist {
38
+ overflow-y: auto;
39
+ flex: 1;
40
+ }
41
+ `
42
+ ]
43
+ }
44
+
45
+ get context() {
46
+ return {
47
+ title: i18next.t('title.data-archive list'),
48
+ help: 'dataset/data-archive',
49
+ actions: [],
50
+ exportable: {
51
+ name: i18next.t('title.data-archive list'),
52
+ data: this._exportableData.bind(this)
53
+ }
54
+ }
55
+ }
56
+
57
+ render() {
58
+ const mode = this.mode || (isMobileDevice() ? 'LIST' : 'GRID')
59
+
60
+ return html`
61
+ <ox-grist
62
+ .mode=${mode}
63
+ .config=${this.gristConfig}
64
+ .fetchHandler=${this.fetchHandler.bind(this)}
65
+ ?url-params-sensitive=${false /* this.active */}
66
+ >
67
+ <div slot="headroom">
68
+ <div id="filters">
69
+ <ox-filters-form autofocus></ox-filters-form>
70
+ </div>
71
+
72
+ <div id="sorters">
73
+ Sort
74
+ <mwc-icon
75
+ @click=${e => {
76
+ const target = e.currentTarget
77
+ this.renderRoot.querySelector('#sorter-control').open({
78
+ right: 0,
79
+ top: target.offsetTop + target.offsetHeight
80
+ })
81
+ }}
82
+ >expand_more</mwc-icon
83
+ >
84
+ <ox-popup id="sorter-control">
85
+ <ox-sorters-control> </ox-sorters-control>
86
+ </ox-popup>
87
+ </div>
88
+
89
+ <div id="modes">
90
+ <mwc-icon @click=${() => (this.mode = 'GRID')} ?active=${mode == 'GRID'}>grid_on</mwc-icon>
91
+ <mwc-icon @click=${() => (this.mode = 'LIST')} ?active=${mode == 'LIST'}>format_list_bulleted</mwc-icon>
92
+ <mwc-icon @click=${() => (this.mode = 'CARD')} ?active=${mode == 'CARD'}>apps</mwc-icon>
93
+ </div>
94
+
95
+ <mwc-button
96
+ dense
97
+ raised
98
+ label=${i18next.t('button.request')}
99
+ @click=${this.openArchivePopup.bind(this)}
100
+ icon="archive"
101
+ ></mwc-button>
102
+ </div>
103
+ </ox-grist>
104
+ `
105
+ }
106
+
107
+ get grist() {
108
+ return this.renderRoot.querySelector('ox-grist')
109
+ }
110
+
111
+ async pageInitialized(lifecycle) {
112
+ const today = new Date().toISOString().split('T')[0]
113
+
114
+ this.gristConfig = {
115
+ list: { fields: ['updater', 'updatedAt'] },
116
+ columns: [
117
+ { type: 'gutter', gutterName: 'sequence' },
118
+ // { type: 'gutter', gutterName: 'row-selector', multiple: true },
119
+ {
120
+ type: 'resource-object',
121
+ name: 'creator',
122
+ header: i18next.t('field.creator'),
123
+ sortable: true,
124
+ width: 120,
125
+ imex: true
126
+ },
127
+ {
128
+ type: 'datetime',
129
+ name: 'createdAt',
130
+ header: i18next.t('field.created_at'),
131
+ sortable: true,
132
+ width: 180,
133
+ imex: true
134
+ },
135
+ {
136
+ type: 'datetime',
137
+ name: 'updatedAt',
138
+ header: i18next.t('field.updated_at'),
139
+ sortable: true,
140
+ width: 180,
141
+ imex: true
142
+ },
143
+ {
144
+ type: 'json5',
145
+ name: 'requestParams',
146
+ header: i18next.t('field.request-params'),
147
+ record: {
148
+ editable: false
149
+ },
150
+ width: 200,
151
+ imex: true
152
+ },
153
+ {
154
+ type: 'string',
155
+ name: 'downloadUrl',
156
+ label: true,
157
+ header: i18next.t('field.download-url'),
158
+ record: {
159
+ editable: false
160
+ },
161
+ width: 240,
162
+ imex: true
163
+ },
164
+ {
165
+ type: 'string',
166
+ name: 'status',
167
+ label: true,
168
+ header: i18next.t('field.status'),
169
+ record: {
170
+ editable: false
171
+ },
172
+ sortable: true,
173
+ width: 120,
174
+ imex: true
175
+ }
176
+ ],
177
+ rows: {
178
+ appendable: false,
179
+ selectable: {
180
+ multiple: true
181
+ }
182
+ },
183
+ sorters: [
184
+ {
185
+ name: 'createdAt',
186
+ desc: true
187
+ }
188
+ ]
189
+ }
190
+ }
191
+
192
+ closePopupAndRefesh() {
193
+ this.popup && this.popup.close()
194
+ this.grist.fetch()
195
+ }
196
+
197
+ async openArchivePopup() {
198
+ this.popup = openPopup(
199
+ html`
200
+ <data-archive-request-popup
201
+ @requested="${this.closePopupAndRefesh.bind(this)}"
202
+ @created="${this.closePopupAndRefesh.bind(this)}"
203
+ ></data-archive-request-popup>`,
204
+ {
205
+ backdrop: true,
206
+ size: 'small',
207
+ title: i18next.t('title.data-archive request popup')
208
+ })
209
+ }
210
+
211
+ async fetchHandler({ page, limit, sortings = [], filters = [] }) {
212
+ const response = await client.query({
213
+ query: gql`
214
+ query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
215
+ responses: dataArchives(filters: $filters, pagination: $pagination, sortings: $sortings) {
216
+ items {
217
+ id
218
+ type
219
+ requestParams
220
+ downloadUrl
221
+ status
222
+ creator {
223
+ id
224
+ name
225
+ }
226
+ updater {
227
+ id
228
+ name
229
+ }
230
+ updatedAt
231
+ createdAt
232
+ }
233
+ total
234
+ }
235
+ }
236
+ `,
237
+ variables: {
238
+ filters,
239
+ pagination: { page, limit },
240
+ sortings
241
+ }
242
+ })
243
+
244
+ return {
245
+ total: response.data.responses.total || 0,
246
+ records: response.data.responses.items || []
247
+ }
248
+ }
249
+
250
+ _exportableData() {
251
+ let records = []
252
+ if (this.grist.selected && this.grist.selected.length > 0) {
253
+ records = this.grist.selected
254
+ } else {
255
+ records = this.grist.data.records
256
+ }
257
+
258
+ var headerSetting = this.grist.compiledConfig.columns
259
+ .filter(column => column.type !== 'gutter' && column.record !== undefined && column.imex !== undefined)
260
+ .map(column => {
261
+ return column.imex === true
262
+ ? {
263
+ header: column.header.renderer(),
264
+ key: column.name,
265
+ width: column.width,
266
+ type: column.type
267
+ }
268
+ : column.imex
269
+ })
270
+
271
+ var data = records.map(item => {
272
+ return {
273
+ id: item.id,
274
+ ...this.gristConfig.columns
275
+ .filter(column => column.type !== 'gutter' && column.record !== undefined && column.imex !== undefined)
276
+ .reduce((record, column) => {
277
+ const key = column.imex === true ? column.name : column.imex.key
278
+ record[key] = key
279
+ .split('.')
280
+ .reduce((obj, key) => (obj && obj[key] !== 'undefined' ? obj[key] : undefined), item)
281
+ return record
282
+ }, {})
283
+ }
284
+ })
285
+
286
+ return { header: headerSetting, data: data }
287
+ }
288
+ }
289
+
290
+ window.customElements.define('data-archive-list-page', DataArchiveListPage)
@@ -0,0 +1,180 @@
1
+ import { SingleColumnFormStyles } from '@things-factory/form-ui'
2
+ import '@things-factory/grist-ui'
3
+ import { i18next, localize } from '@things-factory/i18n-base'
4
+ import { client, CustomAlert } from '@things-factory/shell'
5
+ import gql from 'graphql-tag'
6
+ import { css, html, LitElement } from 'lit-element'
7
+ import moment from 'moment-timezone'
8
+
9
+ class DataArchiveRequestPopup extends localize(i18next)(LitElement) {
10
+ static get properties() {
11
+ return {
12
+ dataSetTypes: Array
13
+ }
14
+ }
15
+
16
+ static get styles() {
17
+ return [
18
+ SingleColumnFormStyles,
19
+ css`
20
+ :host {
21
+ padding: 10px;
22
+ display: flex;
23
+ flex-direction: column;
24
+ overflow-x: overlay;
25
+ background-color: var(--main-section-background-color);
26
+ }
27
+ .button-container {
28
+ padding: var(--button-container-padding);
29
+ margin: var(--button-container-margin);
30
+ text-align: var(--button-container-align);
31
+ background-color: var(--button-container-background);
32
+ height: var(--button-container-height);
33
+ }
34
+ `
35
+ ]
36
+ }
37
+
38
+ render() {
39
+ return html`
40
+ <form id="input-form" name="generation" class="single-column-form" @submit=${e => this.onSubmit(e)}>
41
+ <fieldset>
42
+ <label>${i18next.t('label.start-date')}</label>
43
+ <input type="month" name="startDate" required/>
44
+ <label>${i18next.t('label.end-date')}</label>
45
+ <input type="month" name="endDate" .value=${moment().format('YYYY-MM')}/>
46
+ <label>${i18next.t('label.data-set-type')}</label>
47
+ <select name="type" required>
48
+ ${(this.dataSetTypes || []).map(
49
+ t => html`<option value="${t && t.value}">${t && t.display}</option>`
50
+ )}
51
+ </select>
52
+ </fieldset>
53
+ </form>
54
+
55
+ <div class="button-container">
56
+ <mwc-button raised @click="${this.requestArchive}" label="${i18next.t('button.submit')}"></mwc-button>
57
+ </div>
58
+ `
59
+ }
60
+
61
+ async firstUpdated() {
62
+
63
+ }
64
+
65
+ async onSubmit(e) {
66
+ e.preventDefault()
67
+ this.requestArchive()
68
+ }
69
+
70
+
71
+ serializeFormData() {
72
+ const obj = {}
73
+
74
+ Array.from(this.shadowRoot.querySelectorAll('form#input-form input, select')).forEach(field => {
75
+ if (!field.hasAttribute('hidden') && field.value) {
76
+ obj[field.name] = field.type === 'checkbox' ? field.checked : field.value
77
+ }
78
+ })
79
+
80
+ if (obj['startDate']) {
81
+ obj['startDate'] += '-01'
82
+ }
83
+
84
+ if (obj['endDate']) {
85
+ const endDate = moment(obj['endDate']).endOf('month')
86
+ const today = moment()
87
+ const format = 'YYYY-MM-DD'
88
+
89
+ obj['endDate'] = endDate > today ? today.format(format) : endDate.format(format)
90
+ }
91
+
92
+ return obj
93
+ }
94
+
95
+ /** request download url. */
96
+ async requestArchive() {
97
+ // #1 request download url
98
+ const dataArchive = {
99
+ requestParams: this.serializeFormData(),
100
+ status: 'requested'
101
+ }
102
+
103
+ const response = await client.mutate({
104
+ mutation: gql`
105
+ mutation createDataArchive($dataArchive: NewDataArchive!) {
106
+ createDataArchive(dataArchive: $dataArchive) {
107
+ id
108
+ status
109
+ }
110
+ }
111
+ `,
112
+ variables: { dataArchive }
113
+ })
114
+
115
+ if (!response.errors) {
116
+ this.dispatchEvent(new CustomEvent('requested', {}))
117
+
118
+ const { createDataArchive: { id } } = response.data
119
+ dataArchive['id'] = id
120
+
121
+ this._generateArchiveAndDownloadUrl(dataArchive)
122
+
123
+ await CustomAlert({
124
+ type: 'info',
125
+ title: i18next.t('title.ready'),
126
+ text: i18next.t('text.data-archive waits'),
127
+ confirmButton: { text: i18next.t('button.confirm') }
128
+ })
129
+
130
+ } else {
131
+ console.error(response.errors)
132
+ this.showToast(i18next.t('text.failed'))
133
+ }
134
+ }
135
+
136
+ async _generateArchiveAndDownloadUrl(dataArchive) {
137
+ const response = await client.mutate({
138
+ mutation: gql`
139
+ mutation generatePresignedUrl($patch: DataArchivePatch!) {
140
+ generatePresignedUrl(patch: $patch) {
141
+ id
142
+ downloadUrl
143
+ status
144
+ }
145
+ }
146
+ `,
147
+ variables: { patch: dataArchive }
148
+ })
149
+
150
+ if (!response.errors) {
151
+ this.showToast(i18next.t('title.data-archive downloads ready'))
152
+ this.dispatchEvent(new CustomEvent('created', {}))
153
+ } else {
154
+ console.error(response.errors)
155
+ this.showToast(i18next.t('text.failed'))
156
+ }
157
+ }
158
+
159
+ showToast(message) {
160
+ document.dispatchEvent(new CustomEvent('notify', { detail: { message } }))
161
+ }
162
+
163
+ constructor() {
164
+ super()
165
+
166
+ this.dataSetTypes = [
167
+ {},
168
+ {
169
+ display: i18next.t('text.manually collected'),
170
+ value: 'manual'
171
+ },
172
+ {
173
+ display: i18next.t('text.automatically collected'),
174
+ value: 'automatic'
175
+ }
176
+ ]
177
+ }
178
+ }
179
+
180
+ window.customElements.define('data-archive-request-popup', DataArchiveRequestPopup)
@@ -12,7 +12,40 @@ import { notify, openPopup } from '@operato/layout'
12
12
  import { navigate, PageView, store } from '@operato/shell'
13
13
  import { CommonButtonStyles, CommonGristStyles, ScrollbarStyles } from '@operato/styles'
14
14
  import { isMobileDevice } from '@operato/utils'
15
+ import { encodeUrlParams } from '@things-factory/utils'
15
16
 
17
+ import { getEditor, getRenderer } from '@operato/data-grist'
18
+
19
+ const REPORT_TYPES = [
20
+ {
21
+ display: '',
22
+ value: ''
23
+ },
24
+ {
25
+ display: 'Generated',
26
+ value: 'generated'
27
+ },
28
+ {
29
+ display: 'Embed',
30
+ value: 'embed'
31
+ },
32
+ {
33
+ display: 'Page',
34
+ value: 'page'
35
+ },
36
+ {
37
+ display: 'External URL',
38
+ value: 'external'
39
+ },
40
+ {
41
+ display: 'Jasper',
42
+ value: 'jasper'
43
+ },
44
+ {
45
+ display: 'Shiny',
46
+ value: 'shiny'
47
+ }
48
+ ]
16
49
  export class DataKeySetListPage extends connect(store)(localize(i18next)(PageView)) {
17
50
  static get properties() {
18
51
  return {
@@ -123,7 +156,7 @@ export class DataKeySetListPage extends connect(store)(localize(i18next)(PageVie
123
156
  if (!record.id) return
124
157
  const popup = openPopup(html` <data-key-item-list .dataKeySet=${record}></data-key-item-list> `, {
125
158
  backdrop: true,
126
- help: 'data-set/ui/data-key-item-list',
159
+ help: 'dataset/ui/data-key-item-list',
127
160
  size: 'large',
128
161
  title: i18next.t('title.data-key-item list')
129
162
  })
@@ -136,13 +169,45 @@ export class DataKeySetListPage extends connect(store)(localize(i18next)(PageVie
136
169
  {
137
170
  type: 'gutter',
138
171
  gutterName: 'button',
139
- icon: 'newspaper',
172
+ icon: 'checklist',
140
173
  handlers: {
141
174
  click: (columns, data, column, record, rowIndex) => {
142
175
  navigate(`data-sample-search/${record.id}`)
143
176
  }
144
177
  }
145
178
  },
179
+ {
180
+ type: 'gutter',
181
+ gutterName: 'button',
182
+ icon: 'newspaper',
183
+ handlers: {
184
+ click: (columns, data, column, record, rowIndex) => {
185
+ try {
186
+ const { id:dataKeySetId, reportType, reportView } = record
187
+ switch (reportType) {
188
+ case 'generated':
189
+ break
190
+ case 'page':
191
+ break
192
+ case 'embed':
193
+ break
194
+ case 'external':
195
+ reportView && window.open(reportView, '_blank')
196
+ break
197
+ case 'jasper':
198
+ case 'shiny':
199
+ const urlParams = encodeUrlParams({ dataKeySetId, reportType, reportView })
200
+ navigate(`data-report-samples/?${urlParams}`)
201
+ break
202
+ default:
203
+ break
204
+ }
205
+ } catch(ex) {
206
+ console.log(ex)
207
+ }
208
+ }
209
+ }
210
+ },
146
211
  {
147
212
  type: 'string',
148
213
  name: 'name',
@@ -176,6 +241,34 @@ export class DataKeySetListPage extends connect(store)(localize(i18next)(PageVie
176
241
  sortable: true,
177
242
  width: 60
178
243
  },
244
+ {
245
+ type: 'select',
246
+ name: 'reportType',
247
+ label: true,
248
+ header: i18next.t('field.report-type'),
249
+ record: {
250
+ editable: true,
251
+ options: REPORT_TYPES
252
+ },
253
+ width: 80
254
+ },
255
+ {
256
+ type: 'string',
257
+ name: 'reportView',
258
+ header: i18next.t('field.report-view'),
259
+ record: {
260
+ editable: true,
261
+ editor: function (value, column, record, rowIndex, field) {
262
+ var type = record.reportType !== 'custom' ? 'string' : 'script'
263
+ return getEditor(type)(value, column, record, rowIndex, field)
264
+ },
265
+ renderer: function (value, column, record, rowIndex, field) {
266
+ var type = record.reportType !== 'custom' ? 'string' : 'string'
267
+ return getRenderer(type)(value, column, record, rowIndex, field)
268
+ }
269
+ },
270
+ width: 140
271
+ },
179
272
  {
180
273
  type: 'resource-object',
181
274
  name: 'updater',
@@ -226,6 +319,8 @@ export class DataKeySetListPage extends connect(store)(localize(i18next)(PageVie
226
319
  name
227
320
  description
228
321
  active
322
+ reportType
323
+ reportView
229
324
  dataKeyItems {
230
325
  name
231
326
  description
@@ -315,7 +410,7 @@ export class DataKeySetListPage extends connect(store)(localize(i18next)(PageVie
315
410
 
316
411
  async exportHandler() {
317
412
  const exportTargets = this.grist.selected.length ? this.grist.selected : this.grist.dirtyData.records
318
- const targetFieldSet = new Set(['id', 'name', 'description', 'active'])
413
+ const targetFieldSet = new Set(['id', 'name', 'description', 'active', 'reportType', 'reportView'])
319
414
 
320
415
  return exportTargets.map(dataKeySet => {
321
416
  let tempObj = {}