From 4a9ca29fc05b957a3f2bbbbc7cad13710500dd96 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 18 Oct 2020 17:10:38 +0200 Subject: [PATCH 001/208] Event models #40 --- src/models/Action.js | 93 ++++++++++++++++++++++++++++++++++++++++++++ src/models/Event.js | 40 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/models/Action.js create mode 100644 src/models/Event.js diff --git a/src/models/Action.js b/src/models/Action.js new file mode 100644 index 00000000..4497435e --- /dev/null +++ b/src/models/Action.js @@ -0,0 +1,93 @@ +'use strict' + +const mongoose = require('mongoose') +const uuid = require('uuid').v4 + +const totalSumActionSchema = new mongoose.Schema({ + id: { + type: String, + required: true, + unique: true, + default: uuid + }, + value: { + type: Number, + required: true + }, + created: { + type: Date, + required: true, + index: true, + default: Date.now + }, + updated: { + type: Date, + required: true, + index: true, + default: Date.now + } +}) + +// const keySumActionSchema = new mongoose.Schema({ +// id: { +// type: String, +// required: true, +// unique: true, +// default: uuid +// }, +// key: { +// type: String, +// required: true +// }, +// value: { +// type: Number, +// required: true +// }, +// created: { +// type: Date, +// required: true, +// index: true, +// default: Date.now +// }, +// updated: { +// type: Date, +// required: true, +// index: true, +// default: Date.now +// } +// }) + +// const logActionSchema = new mongoose.Schema({ +// id: { +// type: String, +// required: true, +// unique: true, +// default: uuid +// }, +// summary: { +// type: String, +// required: true +// }, +// details: { +// type: String, +// required: true +// }, +// created: { +// type: Date, +// required: true, +// index: true, +// default: Date.now +// }, +// updated: { +// type: Date, +// required: true, +// index: true, +// default: Date.now +// } +// }) + +module.exports = { + TotalSumAction: mongoose.model('TotalSumAction', totalSumActionSchema) + // KeySumAction: mongoose.model('KeySumAction', keySumActionSchema), + // LongAction: mongoose.model('LogAction', logActionSchema) +} \ No newline at end of file diff --git a/src/models/Event.js b/src/models/Event.js new file mode 100644 index 00000000..7b96f59d --- /dev/null +++ b/src/models/Event.js @@ -0,0 +1,40 @@ +'use strict' + +const mongoose = require('mongoose') +const uuid = require('uuid').v4 + +const isKnownType = (value) => [ + 'TotalSum' + // 'KeySum', + // 'Log' +].includes(value) + +const schema = new mongoose.Schema({ + id: { + type: String, + required: true, + unique: true, + default: uuid + }, + title: { + type: String, + required: true + }, + type: { + type: String, + required: true, + validate: isKnownType + }, + created: { + type: Date, + required: true, + default: Date.now + }, + updated: { + type: Date, + required: true, + default: Date.now + } +}) + +module.exports = mongoose.model('Event', schema) \ No newline at end of file From fa7cf278d8bf8d71d36da89460020f16274e4c85 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 18 Oct 2020 17:10:44 +0200 Subject: [PATCH 002/208] Event types #40 --- src/types/actions.js | 70 ++++++++++++++++++++++++ src/types/events.js | 123 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 src/types/actions.js create mode 100644 src/types/events.js diff --git a/src/types/actions.js b/src/types/actions.js new file mode 100644 index 00000000..a9001a5e --- /dev/null +++ b/src/types/actions.js @@ -0,0 +1,70 @@ +'use strict' + +const { gql } = require('apollo-server-micro') + +module.exports = gql` + """ + Event entries will be stored as actions. The TotalSumAction contains data for a TotalSum type event. + """ + type TotalSumAction { + """ + Action identifier. + """ + id: ID! + """ + Identifies the date and time when the object was created. + """ + created: DateTime! + """ + Identifies the date and time when the object was updated. + """ + updated: DateTime! + } + + input CreateTotalSumActionInput { + """ + Numerical value that is added to all other numerical values of the same day, month or year. + """ + value: Float! + } + + type CreateTotalSumActionPayload { + """ + Indicates that the action creation was successful. Might be 'null' otherwise. + """ + success: Boolean + """ + The newly created action. + """ + payload: TotalSumAction + } + + input UpdateTotalSumActionInput { + """ + Numerical value that is added to all other numerical values of the same day, month or year. + """ + value: Float! + } + + type UpdateTotalSumActionPayload { + """ + Indicates that the action update was successful. Might be 'null' otherwise. + """ + success: Boolean + """ + The updated action. + """ + payload: TotalSumAction + } + + type Mutation { + """ + Create a new action for a TotalSum type event. + """ + createTotalSumAction(eventId: ID!, input: CreateTotalSumActionInput!): CreateTotalSumActionPayload! + """ + Update an existing action for a TotalSum type event. + """ + updateTotalSumAction(id: ID!, input: UpdateTotalSumActionInput!): UpdateTotalSumActionPayload! + } +` \ No newline at end of file diff --git a/src/types/events.js b/src/types/events.js new file mode 100644 index 00000000..f3233087 --- /dev/null +++ b/src/types/events.js @@ -0,0 +1,123 @@ +'use strict' + +const { gql } = require('apollo-server-micro') + +module.exports = gql` + enum EventType { + """ + Event that calculates the sum of all action values grouped by day, month or year. + """ + TOTAL_SUM + """ + Event that calculates the sum of all action values of a key, grouped by day, month or year. + """ + KEY_SUM + """ + Event that shows all actions in a chronic order. + """ + LOG + } + + """ + Events are required to track actions. You can create as many events as you want. This allows you to analyse specific actions happening on your sites. Like a button click or a successful sale. + """ + type Event { + """ + Event identifier. + """ + id: ID! + """ + Title of the event. + """ + title: String! + """ + Type of the event. + """ + type: EventType! + """ + Identifies the date and time when the object was created. + """ + created: DateTime! + """ + Identifies the date and time when the object was updated. + """ + updated: DateTime! + } + + input CreateEventInput { + """ + Title of the event. + """ + title: String! + """ + Type of the event. + """ + type: EventType! + } + + type CreateEventPayload { + """ + Indicates that the event creation was successful. Might be 'null' otherwise. + """ + success: Boolean + """ + The newly created event. + """ + payload: Event + } + + input UpdateEventInput { + """ + Title of the event. + """ + title: String! + """ + Type of the event. + """ + type: EventType! + } + + type UpdateEventPayload { + """ + Indicates that the event update was successful. Might be 'null' otherwise. + """ + success: Boolean + """ + The updated event. + """ + payload: Event + } + + type DeleteEventPayload { + """ + Indicates that the event deletion was successful. Might be 'null' otherwise. + """ + success: Boolean + } + + type Query { + """ + Data of a specific event. + """ + event(id: ID!): Event + """ + Data of all existing events. + """ + events: [Event!] + } + + type Mutation { + """ + Create a new event. + """ + createEvent(input: CreateEventInput!): CreateEventPayload! + """ + Update an existing event. + """ + updateEvent(id: ID!, input: UpdateEventInput!): UpdateEventPayload! + """ + Delete an existing event. + """ + deleteEvent(id: ID!): DeleteEventPayload! + } +` \ No newline at end of file From d8ae7101178c6f9c1d73421a1242d66be78c6d20 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 24 Oct 2020 13:51:11 +0200 Subject: [PATCH 003/208] Adjust types --- src/types/actions.js | 59 +++++++++++++++++++++++++++++++++----------- src/types/events.js | 14 ++++------- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/types/actions.js b/src/types/actions.js index a9001a5e..ba46bf3a 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -4,14 +4,27 @@ const { gql } = require('apollo-server-micro') module.exports = gql` """ - Event entries will be stored as actions. The TotalSumAction contains data for a TotalSum type event. + Event entries will be stored as actions. """ - type TotalSumAction { + type Action { """ Action identifier. """ id: ID! """ + Optional key that will be used to group similar actions in the UI. + """ + key: String + """ + Numerical value that is added to all other numerical values of the key, grouped by day, month or year. + Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. + """ + value: Float! + """ + Details allow you to store more data along with the associated action. + """ + details: String + """ Identifies the date and time when the object was created. """ created: DateTime! @@ -21,14 +34,23 @@ module.exports = gql` updated: DateTime! } - input CreateTotalSumActionInput { + input CreateActionInput { + """ + Optional key that will be used to group similar actions in the UI. """ - Numerical value that is added to all other numerical values of the same day, month or year. + key: String + """ + Numerical value that is added to all other numerical values of the key, grouped by day, month or year. + Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. """ value: Float! + """ + Details allow you to store more data along with the associated action. + """ + details: String } - type CreateTotalSumActionPayload { + type CreateActionPayload { """ Indicates that the action creation was successful. Might be 'null' otherwise. """ @@ -36,17 +58,26 @@ module.exports = gql` """ The newly created action. """ - payload: TotalSumAction + payload: Action } - input UpdateTotalSumActionInput { + input UpdateActionInput { + """ + Optional key that will be used to group similar actions in the UI. """ - Numerical value that is added to all other numerical values of the same day, month or year. + key: String + """ + Numerical value that is added to all other numerical values of the key, grouped by day, month or year. + Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. """ value: Float! + """ + Details allow you to store more data along with the associated action. + """ + details: String } - type UpdateTotalSumActionPayload { + type UpdateActionPayload { """ Indicates that the action update was successful. Might be 'null' otherwise. """ @@ -54,17 +85,17 @@ module.exports = gql` """ The updated action. """ - payload: TotalSumAction + payload: Action } type Mutation { """ - Create a new action for a TotalSum type event. + Create a new action to track an event. """ - createTotalSumAction(eventId: ID!, input: CreateTotalSumActionInput!): CreateTotalSumActionPayload! + createAction(eventId: ID!, input: CreateActionInput!): CreateActionPayload! """ - Update an existing action for a TotalSum type event. + Update an existing action. """ - updateTotalSumAction(id: ID!, input: UpdateTotalSumActionInput!): UpdateTotalSumActionPayload! + updateAction(id: ID!, input: UpdateActionInput!): UpdateActionPayload! } ` \ No newline at end of file diff --git a/src/types/events.js b/src/types/events.js index f3233087..d8821a95 100644 --- a/src/types/events.js +++ b/src/types/events.js @@ -5,17 +5,13 @@ const { gql } = require('apollo-server-micro') module.exports = gql` enum EventType { """ - Event that calculates the sum of all action values grouped by day, month or year. + The UI will display the data of this event as bar chart. """ - TOTAL_SUM + CHART """ - Event that calculates the sum of all action values of a key, grouped by day, month or year. + The UI will display the data of this event as a list of entries. """ - KEY_SUM - """ - Event that shows all actions in a chronic order. - """ - LOG + LIST } """ @@ -31,7 +27,7 @@ module.exports = gql` """ title: String! """ - Type of the event. + Type of the event. Allows you to decide how Ackee should display the data of this event in the UI. """ type: EventType! """ From cf0a17a3b5fc4377a09f94af8eb66b3c07cc9a89 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 24 Oct 2020 14:17:33 +0200 Subject: [PATCH 004/208] Basic setup for events and actions --- src/database/actions.js | 61 ++++++++++++++++++++++++++ src/database/events.js | 88 ++++++++++++++++++++++++++++++++++++++ src/models/Action.js | 77 ++++++--------------------------- src/models/Event.js | 5 +-- src/resolvers/actions.js | 83 ++++++++++++++++++++++++++++++++++++ src/resolvers/events.js | 92 ++++++++++++++++++++++++++++++++++++++++ src/types/actions.js | 4 -- 7 files changed, 339 insertions(+), 71 deletions(-) create mode 100644 src/database/actions.js create mode 100644 src/database/events.js create mode 100644 src/resolvers/actions.js create mode 100644 src/resolvers/events.js diff --git a/src/database/actions.js b/src/database/actions.js new file mode 100644 index 00000000..b1f20f67 --- /dev/null +++ b/src/database/actions.js @@ -0,0 +1,61 @@ +'use strict' + +const Action = require('../models/Action') + +const response = (entry) => ({ + id: entry.id, + key: entry.key, + value: entry.value, + details: entry.details, + created: entry.created, + updated: entry.updated +}) + +const add = async (data) => { + + const enhance = (entry) => { + return entry == null ? entry : response(entry) + } + + return enhance( + await Action.create(data) + ) + +} + +const update = async (id, data) => { + + const enhance = (entry) => { + return entry == null ? entry : response(entry) + } + + return enhance( + await Action.findOneAndUpdate({ + id + }, { + $set: { + key: data.key, + value: data.value, + details: data.details, + updated: Date.now() + } + }, { + new: true + }) + ) + +} + +const del = async (eventId) => { + + return Action.deleteMany({ + eventId + }) + +} + +module.exports = { + add, + update, + del +} \ No newline at end of file diff --git a/src/database/events.js b/src/database/events.js new file mode 100644 index 00000000..8c830ac8 --- /dev/null +++ b/src/database/events.js @@ -0,0 +1,88 @@ +'use strict' + +const Event = require('../models/Event') +const sortByProp = require('../utils/sortByProp') + +const response = (entry) => ({ + id: entry.id, + title: entry.title, + type: entry.type, + created: entry.created, + updated: entry.updated +}) + +const add = async (data) => { + + const enhance = (entry) => { + return entry == null ? entry : response(entry) + } + + return enhance( + await Event.create(data) + ) + +} + +const all = async () => { + + const enhance = (entries) => { + return entries + .map(response) + .sort(sortByProp('title')) + } + + return enhance( + await Event.find({}) + ) + +} + +const get = async (id) => { + + const enhance = (entry) => { + return entry == null ? entry : response(entry) + } + + return enhance( + await Event.findOne({ id }) + ) + +} + +const update = async (id, data) => { + + const enhance = (entry) => { + return entry == null ? entry : response(entry) + } + + return enhance( + await Event.findOneAndUpdate({ + id + }, { + $set: { + title: data.title, + type: data.type, + updated: Date.now() + } + }, { + new: true + }) + ) + +} + +const del = async (id) => { + + return Event.findOneAndDelete({ + id + }) + +} + +module.exports = { + add, + all, + get, + update, + del +} \ No newline at end of file diff --git a/src/models/Action.js b/src/models/Action.js index 4497435e..4d6b4eec 100644 --- a/src/models/Action.js +++ b/src/models/Action.js @@ -3,17 +3,28 @@ const mongoose = require('mongoose') const uuid = require('uuid').v4 -const totalSumActionSchema = new mongoose.Schema({ +const schema = new mongoose.Schema({ id: { type: String, required: true, unique: true, default: uuid }, + eventId: { + type: String, + required: true, + index: true + }, + key: { + type: String + }, value: { type: Number, required: true }, + details: { + type: String + }, created: { type: Date, required: true, @@ -28,66 +39,4 @@ const totalSumActionSchema = new mongoose.Schema({ } }) -// const keySumActionSchema = new mongoose.Schema({ -// id: { -// type: String, -// required: true, -// unique: true, -// default: uuid -// }, -// key: { -// type: String, -// required: true -// }, -// value: { -// type: Number, -// required: true -// }, -// created: { -// type: Date, -// required: true, -// index: true, -// default: Date.now -// }, -// updated: { -// type: Date, -// required: true, -// index: true, -// default: Date.now -// } -// }) - -// const logActionSchema = new mongoose.Schema({ -// id: { -// type: String, -// required: true, -// unique: true, -// default: uuid -// }, -// summary: { -// type: String, -// required: true -// }, -// details: { -// type: String, -// required: true -// }, -// created: { -// type: Date, -// required: true, -// index: true, -// default: Date.now -// }, -// updated: { -// type: Date, -// required: true, -// index: true, -// default: Date.now -// } -// }) - -module.exports = { - TotalSumAction: mongoose.model('TotalSumAction', totalSumActionSchema) - // KeySumAction: mongoose.model('KeySumAction', keySumActionSchema), - // LongAction: mongoose.model('LogAction', logActionSchema) -} \ No newline at end of file +module.exports = mongoose.model('Action', schema) \ No newline at end of file diff --git a/src/models/Event.js b/src/models/Event.js index 7b96f59d..75bfe54b 100644 --- a/src/models/Event.js +++ b/src/models/Event.js @@ -4,9 +4,8 @@ const mongoose = require('mongoose') const uuid = require('uuid').v4 const isKnownType = (value) => [ - 'TotalSum' - // 'KeySum', - // 'Log' + 'CHART', + 'LIST' ].includes(value) const schema = new mongoose.Schema({ diff --git a/src/resolvers/actions.js b/src/resolvers/actions.js new file mode 100644 index 00000000..2cec10d3 --- /dev/null +++ b/src/resolvers/actions.js @@ -0,0 +1,83 @@ +'use strict' + +const KnownError = require('../utils/KnownError') +const messages = require('../utils/messages') +const events = require('../database/events') +const actions = require('../database/actions') + +const polish = (obj) => { + + return Object.entries(obj).reduce((acc, [ key, value ]) => { + + value = typeof value === 'string' ? value.trim() : value + value = value == null ? undefined : value + value = value === '' ? undefined : value + + acc[key] = value + return acc + + }, {}) + +} + +module.exports = { + Mutation: { + createAction: async (parent, { recordId, input }) => { + + const data = polish({ ...input, recordId }) + + const event = await events.get(recordId) + + if (event == null) throw new KnownError('Unknown event') + + let entry + + try { + + entry = await actions.add(data) + + } catch (err) { + + if (err.name === 'ValidationError') { + throw new KnownError(messages(err.errors)) + } + + throw err + + } + + return { + success: true, + payload: entry + } + + }, + updateAction: async (parent, { id, input }) => { + + let entry + + try { + + entry = await actions.update(id, input) + + } catch (err) { + + if (err.name === 'ValidationError') { + throw new KnownError(messages(err.errors)) + } + + throw err + + } + + if (entry == null) { + throw new KnownError('Unknown action') + } + + return { + success: true + } + + } + } +} \ No newline at end of file diff --git a/src/resolvers/events.js b/src/resolvers/events.js new file mode 100644 index 00000000..4fa2d2bd --- /dev/null +++ b/src/resolvers/events.js @@ -0,0 +1,92 @@ +'use strict' + +const actions = require('../database/actions') +const events = require('../database/events') +const KnownError = require('../utils/KnownError') +const messages = require('../utils/messages') +const pipe = require('../utils/pipe') +const requireAuth = require('../middlewares/requireAuth') +const blockDemoMode = require('../middlewares/blockDemoMode') + +module.exports = { + Event: { + facts: (obj) => obj, + statistics: (obj) => obj + }, + Query: { + event: pipe(requireAuth, async (parent, { id }) => { + + return events.get(id) + + }), + events: pipe(requireAuth, async () => { + + return events.all() + + }) + }, + Mutation: { + createEvent: pipe(requireAuth, blockDemoMode, async (parent, { input }) => { + + let entry + + try { + + entry = await events.add(input) + + } catch (err) { + + if (err.name === 'ValidationError') { + throw new KnownError(messages(err.errors)) + } + + throw err + + } + + return { + payload: entry, + success: true + } + + }), + updateEvent: pipe(requireAuth, blockDemoMode, async (parent, { id, input }) => { + + let entry + + try { + + entry = await events.update(id, input) + + } catch (err) { + + if (err.name === 'ValidationError') { + throw new KnownError(messages(err.errors)) + } + + throw err + + } + + if (entry == null) { + throw new KnownError('Unknown event') + } + + return { + payload: entry, + success: true + } + + }), + deleteEvent: pipe(requireAuth, blockDemoMode, async (parent, { id }) => { + + await actions.del(id) + await events.del(id) + + return { + success: true + } + + }) + } +} \ No newline at end of file diff --git a/src/types/actions.js b/src/types/actions.js index ba46bf3a..9470d736 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -82,10 +82,6 @@ module.exports = gql` Indicates that the action update was successful. Might be 'null' otherwise. """ success: Boolean - """ - The updated action. - """ - payload: Action } type Mutation { From 8670cea8ec0c96d909a3db7a21e893add0841cfa Mon Sep 17 00:00:00 2001 From: BetaHuhn <51766171+BetaHuhn@users.noreply.github.com> Date: Sat, 24 Oct 2020 15:04:55 +0200 Subject: [PATCH 005/208] Add source field to record model --- src/models/Record.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/models/Record.js b/src/models/Record.js index 3a90facf..a33ece2b 100644 --- a/src/models/Record.js +++ b/src/models/Record.js @@ -36,6 +36,9 @@ const schema = new mongoose.Schema({ minlength: 2, maxlength: 2 }, + source: { + type: String + }, screenWidth: { type: Number, min: 0, From 4e07ae7564f39cbf8cbf455e10c9220a46b08d5d Mon Sep 17 00:00:00 2001 From: BetaHuhn <51766171+BetaHuhn@users.noreply.github.com> Date: Sat, 24 Oct 2020 15:05:46 +0200 Subject: [PATCH 006/208] Aggregate source with referrer --- src/aggregations/aggregateNewFields.js | 8 ++++++-- src/aggregations/aggregateRecentFields.js | 8 ++++++-- src/aggregations/aggregateTopFields.js | 8 ++++++-- src/database/referrers.js | 8 ++++---- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/aggregations/aggregateNewFields.js b/src/aggregations/aggregateNewFields.js index ea34b719..85e8f210 100644 --- a/src/aggregations/aggregateNewFields.js +++ b/src/aggregations/aggregateNewFields.js @@ -2,7 +2,7 @@ const matchDomains = require('../stages/matchDomains') -module.exports = (ids, properties, limit) => { +module.exports = (ids, properties, limit, or = false) => { const aggregation = [ matchDomains(ids), @@ -28,7 +28,11 @@ module.exports = (ids, properties, limit) => { ] properties.forEach((property) => { - aggregation[0].$match[property] = { $ne: null } + if (or) { + (aggregation[0].$match['$or'] = aggregation[0].$match['$or'] || []).push({ [property]: { $ne: null } }) + } else { + aggregation[0].$match[property] = { $ne: null } + } aggregation[1].$group._id[property] = `$${ property }` }) diff --git a/src/aggregations/aggregateRecentFields.js b/src/aggregations/aggregateRecentFields.js index 0bac2762..1edbfdd3 100644 --- a/src/aggregations/aggregateRecentFields.js +++ b/src/aggregations/aggregateRecentFields.js @@ -2,7 +2,7 @@ const matchDomains = require('../stages/matchDomains') -module.exports = (ids, properties, limit) => { +module.exports = (ids, properties, limit, or = false) => { const aggregation = [ matchDomains(ids), @@ -23,7 +23,11 @@ module.exports = (ids, properties, limit) => { ] properties.forEach((property) => { - aggregation[0].$match[property] = { $ne: null } + if (or) { + (aggregation[0].$match['$or'] = aggregation[0].$match['$or'] || []).push({ [property]: { $ne: null } }) + } else { + aggregation[0].$match[property] = { $ne: null } + } aggregation[2].$project._id[property] = `$${ property }` }) diff --git a/src/aggregations/aggregateTopFields.js b/src/aggregations/aggregateTopFields.js index e294f9ab..6636e6f5 100644 --- a/src/aggregations/aggregateTopFields.js +++ b/src/aggregations/aggregateTopFields.js @@ -3,7 +3,7 @@ const ranges = require('../constants/ranges') const matchDomains = require('../stages/matchDomains') -module.exports = (ids, properties, range, limit, dateDetails) => { +module.exports = (ids, properties, range, limit, dateDetails, or = false) => { const aggregation = [ matchDomains(ids), @@ -26,7 +26,11 @@ module.exports = (ids, properties, range, limit, dateDetails) => { ] properties.forEach((property) => { - aggregation[0].$match[property] = { $ne: null } + if (or) { + (aggregation[0].$match['$or'] = aggregation[0].$match['$or'] || []).push({ [property]: { $ne: null } }) + } else { + aggregation[0].$match[property] = { $ne: null } + } aggregation[1].$group._id[property] = `$${ property }` }) diff --git a/src/database/referrers.js b/src/database/referrers.js index d0c365e2..90c74c6f 100644 --- a/src/database/referrers.js +++ b/src/database/referrers.js @@ -11,7 +11,7 @@ const get = async (ids, sorting, range, limit, dateDetails) => { const enhance = (entries) => { return entries.map((entry) => ({ - id: entry._id.siteReferrer, + id: entry._id.siteReferrer || entry._id.source, count: entry.count, created: entry.created })) @@ -20,9 +20,9 @@ const get = async (ids, sorting, range, limit, dateDetails) => { const aggregation = (() => { - if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'siteReferrer' ], range, limit, dateDetails) - if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'siteReferrer' ], limit) - if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'siteReferrer' ], limit) + if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'siteReferrer', 'source' ], range, limit, dateDetails, true) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'siteReferrer', 'source' ], limit, true) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'siteReferrer', 'source' ], limit, true) })() From 1895a79e8e16f10558c21c30d6a7a2109196a386 Mon Sep 17 00:00:00 2001 From: BetaHuhn <51766171+BetaHuhn@users.noreply.github.com> Date: Sat, 24 Oct 2020 15:07:14 +0200 Subject: [PATCH 007/208] add source to CreateRecordInput type --- src/types/records.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/types/records.js b/src/types/records.js index af5fb182..0c831d86 100644 --- a/src/types/records.js +++ b/src/types/records.js @@ -18,7 +18,7 @@ module.exports = gql` """ Where the user came from. Either unknown, a specific page or just the domain. This depends on the browser of the user. """ - siteReferrer: URL + siteReferrer: String """ Preferred language of the user. ISO 639-1 formatted. """ @@ -87,6 +87,10 @@ module.exports = gql` """ siteReferrer: URL """ + Where the user came from. + """ + source: String + """ Preferred language of the user. ISO 639-1 formatted. """ siteLanguage: String From b7bf459a1494a5d7a26f4b447eba3facab6dc723 Mon Sep 17 00:00:00 2001 From: BetaHuhn <51766171+BetaHuhn@users.noreply.github.com> Date: Sat, 24 Oct 2020 15:56:19 +0200 Subject: [PATCH 008/208] Remove default value for or --- src/aggregations/aggregateNewFields.js | 2 +- src/aggregations/aggregateRecentFields.js | 2 +- src/aggregations/aggregateTopFields.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aggregations/aggregateNewFields.js b/src/aggregations/aggregateNewFields.js index 85e8f210..a04fbcb3 100644 --- a/src/aggregations/aggregateNewFields.js +++ b/src/aggregations/aggregateNewFields.js @@ -2,7 +2,7 @@ const matchDomains = require('../stages/matchDomains') -module.exports = (ids, properties, limit, or = false) => { +module.exports = (ids, properties, limit, or) => { const aggregation = [ matchDomains(ids), diff --git a/src/aggregations/aggregateRecentFields.js b/src/aggregations/aggregateRecentFields.js index 1edbfdd3..00746656 100644 --- a/src/aggregations/aggregateRecentFields.js +++ b/src/aggregations/aggregateRecentFields.js @@ -2,7 +2,7 @@ const matchDomains = require('../stages/matchDomains') -module.exports = (ids, properties, limit, or = false) => { +module.exports = (ids, properties, limit, or) => { const aggregation = [ matchDomains(ids), diff --git a/src/aggregations/aggregateTopFields.js b/src/aggregations/aggregateTopFields.js index 6636e6f5..4427d196 100644 --- a/src/aggregations/aggregateTopFields.js +++ b/src/aggregations/aggregateTopFields.js @@ -3,7 +3,7 @@ const ranges = require('../constants/ranges') const matchDomains = require('../stages/matchDomains') -module.exports = (ids, properties, range, limit, dateDetails, or = false) => { +module.exports = (ids, properties, range, limit, dateDetails, or) => { const aggregation = [ matchDomains(ids), From 1e738323d45d382829b4161c1578ebc2d8787ae4 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 24 Oct 2020 16:05:42 +0200 Subject: [PATCH 009/208] Add more props to select field --- src/ui/scripts/components/Select.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/components/Select.js b/src/ui/scripts/components/Select.js index 2ab5fb7e..f88e4b83 100644 --- a/src/ui/scripts/components/Select.js +++ b/src/ui/scripts/components/Select.js @@ -6,9 +6,11 @@ const Select = (props) => { return ( h('select', { className: 'select', + id: props.id, + required: props.required, + disabled: props.disabled, value: props.value, - onChange: props.onChange, - disabled: props.disabled + onChange: props.onChange }, props.items.map((item, index) => ( h('option', { @@ -22,6 +24,9 @@ const Select = (props) => { } Select.propTypes = { + id: PropTypes.string, + required: PropTypes.bool, + disabled: PropTypes.bool, value: PropTypes.string, onChange: PropTypes.func.isRequired, items: PropTypes.arrayOf( From 46384102ca73cb60bcf6bc5396fdb046a4de8842 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 24 Oct 2020 17:11:09 +0200 Subject: [PATCH 010/208] Use constants for events --- src/constants/events.js | 9 +++++++++ src/models/Event.js | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/constants/events.js diff --git a/src/constants/events.js b/src/constants/events.js new file mode 100644 index 00000000..20119e81 --- /dev/null +++ b/src/constants/events.js @@ -0,0 +1,9 @@ +'use strict' + +const EVENTS_TYPE_CHART = 'CHART' +const EVENTS_TYPE_LIST = 'LIST' + +module.exports = { + EVENTS_TYPE_CHART, + EVENTS_TYPE_LIST +} \ No newline at end of file diff --git a/src/models/Event.js b/src/models/Event.js index 75bfe54b..7661d785 100644 --- a/src/models/Event.js +++ b/src/models/Event.js @@ -3,9 +3,11 @@ const mongoose = require('mongoose') const uuid = require('uuid').v4 +const events = require('../constants/events') + const isKnownType = (value) => [ - 'CHART', - 'LIST' + events.EVENTS_TYPE_CHART, + events.EVENTS_TYPE_LIST ].includes(value) const schema = new mongoose.Schema({ From 388844f7aa339389eef14ed1f1e1b9213ce892d3 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 24 Oct 2020 17:15:13 +0200 Subject: [PATCH 011/208] Events in API and UI --- src/resolvers/events.js | 4 - src/resolvers/index.js | 2 + src/types/index.js | 2 + src/ui/scripts/actions/events.js | 170 ++++++++++++++++++ src/ui/scripts/actions/index.js | 1 + src/ui/scripts/components/Dashboard.js | 5 + src/ui/scripts/components/Modals.js | 30 +++- .../components/modals/ModalEventAdd.js | 111 ++++++++++++ .../components/modals/ModalEventEdit.js | 160 +++++++++++++++++ .../scripts/components/routes/RouteEvents.js | 35 ++++ .../components/routes/RouteSettings.js | 61 +++++-- src/ui/scripts/constants/modals.js | 4 +- src/ui/scripts/constants/route.js | 6 +- src/ui/scripts/reducers/events.js | 29 +++ src/ui/scripts/reducers/index.js | 2 + 15 files changed, 599 insertions(+), 23 deletions(-) create mode 100644 src/ui/scripts/actions/events.js create mode 100644 src/ui/scripts/components/modals/ModalEventAdd.js create mode 100644 src/ui/scripts/components/modals/ModalEventEdit.js create mode 100644 src/ui/scripts/components/routes/RouteEvents.js create mode 100644 src/ui/scripts/reducers/events.js diff --git a/src/resolvers/events.js b/src/resolvers/events.js index 4fa2d2bd..bdf90989 100644 --- a/src/resolvers/events.js +++ b/src/resolvers/events.js @@ -9,10 +9,6 @@ const requireAuth = require('../middlewares/requireAuth') const blockDemoMode = require('../middlewares/blockDemoMode') module.exports = { - Event: { - facts: (obj) => obj, - statistics: (obj) => obj - }, Query: { event: pipe(requireAuth, async (parent, { id }) => { diff --git a/src/resolvers/index.js b/src/resolvers/index.js index 50ee8614..39839169 100644 --- a/src/resolvers/index.js +++ b/src/resolvers/index.js @@ -6,6 +6,8 @@ module.exports = mergeResolvers([ require('./tokens'), require('./records'), require('./domains'), + require('./actions'), + require('./events'), require('./facts'), require('./statistics') ]) \ No newline at end of file diff --git a/src/types/index.js b/src/types/index.js index 7ac7fc7c..7af2ac21 100644 --- a/src/types/index.js +++ b/src/types/index.js @@ -4,6 +4,8 @@ const { mergeTypeDefs } = require('graphql-tools') module.exports = mergeTypeDefs([ require('./domains'), + require('./events'), + require('./actions'), require('./facts'), require('./miscellaneous'), require('./records'), diff --git a/src/ui/scripts/actions/events.js b/src/ui/scripts/actions/events.js new file mode 100644 index 00000000..07900665 --- /dev/null +++ b/src/ui/scripts/actions/events.js @@ -0,0 +1,170 @@ +import api from '../utils/api' +import signalHandler from '../utils/signalHandler' + +export const SET_EVENTS_VALUE = Symbol() +export const SET_EVENTS_FETCHING = Symbol() +export const SET_EVENTS_ERROR = Symbol() + +export const setEventsValue = (payload) => ({ + type: SET_EVENTS_VALUE, + payload +}) + +export const setEventsFetching = (payload) => ({ + type: SET_EVENTS_FETCHING, + payload +}) + +export const setEventsError = (payload) => ({ + type: SET_EVENTS_ERROR, + payload +}) + +export const fetchEvents = signalHandler((signal) => (props) => async (dispatch) => { + + dispatch(setEventsFetching(true)) + dispatch(setEventsError()) + + try { + + const data = await api({ + query: ` + query fetchEvents { + events { + id + title + type + } + } + `, + props, + signal: signal() + }) + + dispatch(setEventsValue(data.events)) + dispatch(setEventsFetching(false)) + + } catch (err) { + + if (err.name === 'AbortError') return + dispatch(setEventsFetching(false)) + if (err.name === 'HandledError') return + dispatch(setEventsError(err)) + + } + +}) + +export const addEvent = (props, state) => async (dispatch) => { + + dispatch(setEventsFetching(true)) + dispatch(setEventsError()) + + try { + + await api({ + query: ` + mutation createEvent($input: CreateEventInput!) { + createEvent(input: $input) { + success + } + } + `, + variables: { + input: { + title: state.title, + type: state.type + } + }, + props + }) + + await dispatch(fetchEvents(props)) + dispatch(setEventsFetching(false)) + + } catch (err) { + + if (err.name === 'AbortError') return + dispatch(setEventsFetching(false)) + if (err.name === 'HandledError') return + dispatch(setEventsError(err)) + + } + +} + +export const updateEvent = signalHandler((signal) => (props, eventId, state) => async (dispatch) => { + + dispatch(setEventsFetching(true)) + dispatch(setEventsError()) + + try { + + await api({ + query: ` + mutation updateEvent($id: ID!, $input: UpdateEventInput!) { + updateEvent(id: $id, input: $input) { + success + } + } + `, + variables: { + id: eventId, + input: { + title: state.title, + type: state.type + } + }, + props, + signal: signal(eventId) + }) + + await dispatch(fetchEvents(props)) + dispatch(setEventsFetching(false)) + + } catch (err) { + + if (err.name === 'AbortError') return + dispatch(setEventsFetching(false)) + if (err.name === 'HandledError') return + dispatch(setEventsError(err)) + + } + +}) + +export const deleteEvent = signalHandler((signal) => (props, eventId) => async (dispatch) => { + + dispatch(setEventsFetching(true)) + dispatch(setEventsError()) + + try { + + await api({ + query: ` + mutation deleteEvent($id: ID!) { + deleteEvent(id: $id) { + success + } + } + `, + variables: { + id: eventId + }, + props, + signal: signal(eventId) + }) + + await dispatch(fetchEvents(props)) + dispatch(setEventsFetching(false)) + + } catch (err) { + + if (err.name === 'AbortError') return + dispatch(setEventsFetching(false)) + if (err.name === 'HandledError') return + dispatch(setEventsError(err)) + + } + +}) \ No newline at end of file diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index b71ee31f..63ee1571 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -3,6 +3,7 @@ export * from './token' export * from './route' export * from './filter' export * from './domains' +export * from './events' export * from './overview' export * from './views' export * from './pages' diff --git a/src/ui/scripts/components/Dashboard.js b/src/ui/scripts/components/Dashboard.js index f21d9f28..4bb61133 100644 --- a/src/ui/scripts/components/Dashboard.js +++ b/src/ui/scripts/components/Dashboard.js @@ -25,6 +25,7 @@ const Dashboard = (props) => { useEffect(() => { props.fetchDomains(props) + props.fetchEvents(props) }, []) @@ -41,6 +42,8 @@ const Dashboard = (props) => { useHotkeys('r', () => props.setRoute(route.ROUTE_REFERRERS)) useHotkeys('d', () => props.setRoute(route.ROUTE_DURATIONS)) + useHotkeys('e', () => props.setRoute(route.ROUTE_EVENTS)) + useHotkeys('0', () => gotoDomainWhenDefined(props, 0), [ props ]) useHotkeys('1', () => gotoDomainWhenDefined(props, 1), [ props ]) useHotkeys('2', () => gotoDomainWhenDefined(props, 2), [ props ]) @@ -67,6 +70,8 @@ const Dashboard = (props) => { createDropdownButton(route.ROUTE_REFERRERS.title, route.ROUTE_REFERRERS, props, 'r'), createDropdownButton(route.ROUTE_DURATIONS.title, route.ROUTE_DURATIONS, props, 'd'), createDropdownSeparator(), + createDropdownButton(route.ROUTE_EVENTS.title, route.ROUTE_EVENTS, props, 'e'), + createDropdownSeparator(), createDropdownButton(route.ROUTE_SYSTEMS.title, route.ROUTE_SYSTEMS, props), createDropdownButton(route.ROUTE_DEVICES.title, route.ROUTE_DEVICES, props), createDropdownButton(route.ROUTE_BROWSERS.title, route.ROUTE_BROWSERS, props), diff --git a/src/ui/scripts/components/Modals.js b/src/ui/scripts/components/Modals.js index 04d1949d..2cf32a79 100644 --- a/src/ui/scripts/components/Modals.js +++ b/src/ui/scripts/components/Modals.js @@ -1,13 +1,17 @@ import { createElement as h, Fragment } from 'react' import { + MODALS_DOMAIN_ADD, MODALS_DOMAIN_EDIT, - MODALS_DOMAIN_ADD + MODALS_EVENT_ADD, + MODALS_EVENT_EDIT } from '../constants/modals' import Modal from './modals/Modal' -import ModalDomainEdit from './modals/ModalDomainEdit' import ModalDomainAdd from './modals/ModalDomainAdd' +import ModalDomainEdit from './modals/ModalDomainEdit' +import ModalEventAdd from './modals/ModalEventAdd' +import ModalEventEdit from './modals/ModalEventEdit' const Modals = (props) => { @@ -18,6 +22,12 @@ const Modals = (props) => { return ( h(Modal, { key: modalId, visible: modalData.visible }, + modalData.type === MODALS_DOMAIN_ADD && h(ModalDomainAdd, { + current, + fetching: props.domains.fetching, + addDomain: props.addDomain.bind(null, props), + closeModal + }), modalData.type === MODALS_DOMAIN_EDIT && h(ModalDomainEdit, { current, id: modalData.props.id, @@ -27,10 +37,20 @@ const Modals = (props) => { deleteDomain: props.deleteDomain.bind(null, props), closeModal }), - modalData.type === MODALS_DOMAIN_ADD && h(ModalDomainAdd, { + modalData.type === MODALS_EVENT_ADD && h(ModalEventAdd, { current, - fetching: props.domains.fetching, - addDomain: props.addDomain.bind(null, props), + fetching: props.events.fetching, + addEvent: props.addEvent.bind(null, props), + closeModal + }), + modalData.type === MODALS_EVENT_EDIT && h(ModalEventEdit, { + current, + id: modalData.props.id, + title: modalData.props.title, + type: modalData.props.type, + fetching: props.events.fetching, + updateEvent: props.updateEvent.bind(null, props), + deleteEvent: props.deleteEvent.bind(null, props), closeModal }) ) diff --git a/src/ui/scripts/components/modals/ModalEventAdd.js b/src/ui/scripts/components/modals/ModalEventAdd.js new file mode 100644 index 00000000..6f278a5d --- /dev/null +++ b/src/ui/scripts/components/modals/ModalEventAdd.js @@ -0,0 +1,111 @@ +import { createElement as h, useState } from 'react' +import PropTypes from 'prop-types' +// import { useHotkeys } from 'react-hotkeys-hook' + +import * as events from '../../../../constants/events' + +import Input from '../Input' +import Select from '../Select' +import Label from '../Label' +import Spinner from '../Spinner' +import Spacer from '../Spacer' + +import shortId from '../../utils/shortId' + +const ModalEventAdd = (props) => { + + // Currently not possible: + // https://github.com/JohannesKlauss/react-hotkeys-hook/issues/276 + // useHotkeys('esc', props.closeModal, { + // filter: () => props.current === true + // }) + + const [ inputs, setInputs ] = useState({ + title: '', + type: events.EVENTS_TYPE_CHART + }) + + const onChange = (key) => (e) => setInputs({ + ...inputs, + [key]: e.target.value + }) + + const addEvent = (e) => { + e.preventDefault() + props.addEvent(inputs).then(props.closeModal) + } + + const titleId = shortId() + const typeId = shortId() + + return ( + h('form', { className: 'card', onSubmit: addEvent }, + h('div', { className: 'card__inner' }, + + h(Spacer, { size: 0.5 }), + + h(Label, { htmlFor: titleId }, 'Event title'), + + h(Input, { + type: 'text', + id: titleId, + required: true, + disabled: props.fetching === true, + focused: true, + placeholder: 'Event title', + value: inputs.title, + onChange: onChange('title') + }), + + h(Label, { htmlFor: typeId }, 'Event type'), + + h(Select, { + id: typeId, + required: true, + disabled: props.fetching === true, + value: inputs.type, + items: [ + { + value: events.EVENTS_TYPE_CHART, + label: 'Chart' + }, + { + value: events.EVENTS_TYPE_LIST, + label: 'List' + } + ], + onChange: onChange('type') + }) + + ), + h('div', { className: 'card__footer' }, + + h('button', { + type: 'button', + className: 'card__button link', + onClick: props.closeModal + }, 'Close'), + + h('div', { + className: 'card__separator' + }), + + h('button', { + className: 'card__button card__button--primary link color-white', + disabled: props.fetching === true + }, props.fetching === true ? h(Spinner) : 'Add') + + ) + ) + ) + +} + +ModalEventAdd.propTypes = { + current: PropTypes.bool.isRequired, + fetching: PropTypes.bool.isRequired, + addEvent: PropTypes.func.isRequired, + closeModal: PropTypes.func.isRequired +} + +export default ModalEventAdd \ No newline at end of file diff --git a/src/ui/scripts/components/modals/ModalEventEdit.js b/src/ui/scripts/components/modals/ModalEventEdit.js new file mode 100644 index 00000000..3db7952c --- /dev/null +++ b/src/ui/scripts/components/modals/ModalEventEdit.js @@ -0,0 +1,160 @@ +import { createElement as h, useState } from 'react' +import PropTypes from 'prop-types' +// import { useHotkeys } from 'react-hotkeys-hook' + +import * as events from '../../../../constants/events' + +import Input from '../Input' +import Select from '../Select' +import Textarea from '../Textarea' +import Label from '../Label' +import Spinner from '../Spinner' +import Spacer from '../Spacer' + +import shortId from '../../utils/shortId' + +const ModalEventEdit = (props) => { + + // Currently not possible: + // https://github.com/JohannesKlauss/react-hotkeys-hook/issues/276 + // useHotkeys('esc', props.closeModal, { + // filter: () => props.current === true + // }) + + const [ inputs, setInputs ] = useState({ + title: props.title, + type: props.type + }) + + const onChange = (key) => (e) => setInputs({ + ...inputs, + [key]: e.target.value + }) + + const copyInput = (e) => { + e.target.select() + document.execCommand('copy') + } + + const updateEvent = (e) => { + e.preventDefault() + props.updateEvent(props.id, inputs).then(props.closeModal) + } + + const deleteEvent = (e) => { + e.preventDefault() + const c = confirm(`Are you sure you want to delete the event "${ props.title }"? This action cannot be undone.`) + if (c === true) props.deleteEvent(props.id, inputs).then(props.closeModal) + } + + const titleId = shortId() + const typeId = shortId() + const idId = shortId() + const embedId = shortId() + + return ( + h('form', { className: 'card', onSubmit: updateEvent }, + h('div', { className: 'card__inner' }, + + h(Spacer, { size: 0.5 }), + + h(Label, { htmlFor: titleId }, 'Event title'), + + h(Input, { + type: 'text', + id: titleId, + required: true, + disabled: props.fetching === true, + focused: true, + placeholder: 'Event title', + value: inputs.title, + onChange: onChange('title') + }), + + h(Label, { htmlFor: typeId }, 'Event type'), + + h(Select, { + id: typeId, + required: true, + disabled: props.fetching === true, + value: inputs.type, + items: [ + { + value: events.EVENTS_TYPE_CHART, + label: 'Chart' + }, + { + value: events.EVENTS_TYPE_LIST, + label: 'List' + } + ], + onChange: onChange('type') + }), + + h(Label, { htmlFor: idId }, 'Event id'), + + h(Input, { + type: 'text', + id: idId, + readOnly: true, + placeholder: 'Event id', + value: props.id, + onFocus: copyInput + }), + + h(Label, { htmlFor: embedId }, 'Embed code'), + + h(Textarea, { + type: 'text', + id: embedId, + readOnly: true, + rows: 4, + value: `ackeeTracker.create`, + onFocus: copyInput + }) + + ), + h('div', { className: 'card__footer' }, + + h('button', { + type: 'button', + className: 'card__button link', + onClick: props.closeModal + }, 'Close'), + + h('div', { + className: 'card__separator ' + }), + + h('button', { + type: 'button', + className: 'card__button link color-destructive', + onClick: deleteEvent + }, 'Delete'), + + h('div', { + className: 'card__separator' + }), + + h('button', { + className: 'card__button card__button--primary link color-white', + disabled: props.fetching === true + }, props.fetching === true ? h(Spinner) : 'Edit') + + ) + ) + ) + +} + +ModalEventEdit.propTypes = { + current: PropTypes.bool.isRequired, + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + fetching: PropTypes.bool.isRequired, + updateEvent: PropTypes.func.isRequired, + deleteEvent: PropTypes.func.isRequired, + closeModal: PropTypes.func.isRequired +} + +export default ModalEventEdit \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteEvents.js b/src/ui/scripts/components/routes/RouteEvents.js new file mode 100644 index 00000000..83d8fcc1 --- /dev/null +++ b/src/ui/scripts/components/routes/RouteEvents.js @@ -0,0 +1,35 @@ +import { createElement as h, Fragment } from 'react' + +// import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' +// import selectViewsValue from '../../selectors/selectViewsValue' +// import enhanceViews from '../../enhancers/enhanceViews' +// import mergeViews from '../../utils/mergeViews' +// import overviewRoute from '../../utils/overviewRoute' + +import CardViews from '../cards/CardViews' + +const RouteEvents = (props) => { + + return ( + h(Fragment, {}, + + props.events.value.map( + (event) => ( + h(CardViews, { + key: event.id, + headline: event.title, + interval: props.filter.interval, + // loading: props.views.fetching, + loading: false, + // items: enhanceViews(selectViewsValue(props, domain.id).value, 7), + items: [] + }) + ) + ) + + ) + ) + +} + +export default RouteEvents \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteSettings.js b/src/ui/scripts/components/routes/RouteSettings.js index 01960fa7..ac92b0ce 100644 --- a/src/ui/scripts/components/routes/RouteSettings.js +++ b/src/ui/scripts/components/routes/RouteSettings.js @@ -1,7 +1,7 @@ import { createElement as h, Fragment } from 'react' import { version, homepage } from '../../../../../package' -import { MODALS_DOMAIN_EDIT, MODALS_DOMAIN_ADD } from '../../constants/modals' +import { MODALS_DOMAIN_ADD, MODALS_DOMAIN_EDIT, MODALS_EVENT_ADD, MODALS_EVENT_EDIT } from '../../constants/modals' import CardSetting from '../cards/CardSetting' import LinkItem from '../LinkItem' @@ -10,23 +10,20 @@ import Message from '../Message' const RouteSettings = (props) => { - const showDomainEditModal = (id, title) => { + const showDomainAddModal = () => { props.addModalsModal({ - type: MODALS_DOMAIN_EDIT, - props: { - id, - title - } + type: MODALS_DOMAIN_ADD, + props: {} }) } - const showDomainAddModal = () => { + const showDomainEditModal = (domain) => { props.addModalsModal({ - type: MODALS_DOMAIN_ADD, - props: {} + type: MODALS_DOMAIN_EDIT, + props: domain }) } @@ -41,7 +38,7 @@ const RouteSettings = (props) => { h(LinkItem, { type: 'button', text: domain.id, - onClick: () => showDomainEditModal(domain.id, domain.title) + onClick: () => showDomainEditModal(domain) }, domain.title), h(Line) ] @@ -49,6 +46,42 @@ const RouteSettings = (props) => { h(LinkItem, { type: 'button', onClick: showDomainAddModal }, 'New domain') ] + const showEventAddModal = () => { + + props.addModalsModal({ + type: MODALS_EVENT_ADD, + props: {} + }) + + } + + const showEventEditModal = (event) => { + + props.addModalsModal({ + type: MODALS_EVENT_EDIT, + props: event + }) + + } + + const eventsFetching = [ + h(Message, { status: 'warning' }, 'Fetching events...') + ] + + const eventsItems = [ + ...props.events.value.map( + (event) => [ + h(LinkItem, { + type: 'button', + text: event.id, + onClick: () => showEventEditModal(event) + }, event.title), + h(Line) + ] + ).flat(), + h(LinkItem, { type: 'button', onClick: showEventAddModal }, 'New event') + ] + return ( h(Fragment, {}, @@ -66,6 +99,12 @@ const RouteSettings = (props) => { ...(props.domains.fetching === true ? domainsFetching : domainsItems) ), + h(CardSetting, { + headline: 'Events' + }, + ...(props.events.fetching === true ? eventsFetching : eventsItems) + ), + h(CardSetting, { headline: 'Donate' }, diff --git a/src/ui/scripts/constants/modals.js b/src/ui/scripts/constants/modals.js index 9b628ffd..817fde4c 100644 --- a/src/ui/scripts/constants/modals.js +++ b/src/ui/scripts/constants/modals.js @@ -1,2 +1,4 @@ +export const MODALS_DOMAIN_ADD = 'modals_domain_add' export const MODALS_DOMAIN_EDIT = 'modals_domain_edit' -export const MODALS_DOMAIN_ADD = 'modals_domain_add' \ No newline at end of file +export const MODALS_EVENT_ADD = 'modals_event_add' +export const MODALS_EVENT_EDIT = 'modals_event_edit' \ No newline at end of file diff --git a/src/ui/scripts/constants/route.js b/src/ui/scripts/constants/route.js index 13e50b7c..00762045 100644 --- a/src/ui/scripts/constants/route.js +++ b/src/ui/scripts/constants/route.js @@ -8,6 +8,7 @@ import RouteDevices from '../components/routes/RouteDevices' import RouteLanguages from '../components/routes/RouteLanguages' import RouteSystems from '../components/routes/RouteSystems' import RouteSizes from '../components/routes/RouteSizes' +import RouteEvents from '../components/routes/RouteEvents' import RouteSettings from '../components/routes/RouteSettings' export const ROUTE_OVERVIEW = { key: 'route_overview', title: 'Overview', component: RouteOverview } @@ -18,6 +19,7 @@ export const ROUTE_DURATIONS = { key: 'route_durations', title: 'Durations', com export const ROUTE_LANGUAGES = { key: 'route_languages', title: 'Languages', component: RouteLanguages } export const ROUTE_SIZES = { key: 'route_sizes', title: 'Sizes', component: RouteSizes } export const ROUTE_SYSTEMS = { key: 'route_systems', title: 'Systems', component: RouteSystems } -export const ROUTE_SETTINGS = { key: 'route_settings', title: 'Settings', component: RouteSettings } export const ROUTE_DEVICES = { key: 'route_devices', title: 'Devices', component: RouteDevices } -export const ROUTE_BROWSERS = { key: 'route_browsers', title: 'Browsers', component: RouteBrowsers } \ No newline at end of file +export const ROUTE_BROWSERS = { key: 'route_browsers', title: 'Browsers', component: RouteBrowsers } +export const ROUTE_EVENTS = { key: 'route_events', title: 'Events', component: RouteEvents } +export const ROUTE_SETTINGS = { key: 'route_settings', title: 'Settings', component: RouteSettings } \ No newline at end of file diff --git a/src/ui/scripts/reducers/events.js b/src/ui/scripts/reducers/events.js new file mode 100644 index 00000000..3df11af4 --- /dev/null +++ b/src/ui/scripts/reducers/events.js @@ -0,0 +1,29 @@ +import produce from 'immer' + +import { + SET_EVENTS_VALUE, + SET_EVENTS_FETCHING, + SET_EVENTS_ERROR +} from '../actions' + +export const initialState = () => ({ + value: [], + fetching: false, + error: undefined +}) + +export default produce((draft, action) => { + + switch (action.type) { + case SET_EVENTS_VALUE: + draft.value = action.payload || initialState().value + break + case SET_EVENTS_FETCHING: + draft.fetching = action.payload || initialState().fetching + break + case SET_EVENTS_ERROR: + draft.error = action.payload || initialState().error + break + } + +}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/index.js b/src/ui/scripts/reducers/index.js index 8e17484a..4c6f6690 100644 --- a/src/ui/scripts/reducers/index.js +++ b/src/ui/scripts/reducers/index.js @@ -9,6 +9,7 @@ import token from './token' import route from './route' import filter from './filter' import domains from './domains' +import events from './events' import overview from './overview' import views from './views' import pages from './pages' @@ -26,6 +27,7 @@ const reducers = combineReducers({ route, filter, domains, + events, overview, views, pages, From 0b52ea5da7780b9dc820a5e2a376143f524044bf Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 24 Oct 2020 17:18:23 +0200 Subject: [PATCH 012/208] Remove empty space in className --- src/ui/scripts/components/modals/ModalDomainAdd.js | 2 +- src/ui/scripts/components/modals/ModalDomainEdit.js | 4 ++-- src/ui/scripts/components/modals/ModalEventEdit.js | 2 +- src/ui/scripts/components/overlays/OverlayFailure.js | 2 +- src/ui/scripts/components/overlays/OverlayLogin.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/scripts/components/modals/ModalDomainAdd.js b/src/ui/scripts/components/modals/ModalDomainAdd.js index 67956384..1e3873eb 100644 --- a/src/ui/scripts/components/modals/ModalDomainAdd.js +++ b/src/ui/scripts/components/modals/ModalDomainAdd.js @@ -62,7 +62,7 @@ const ModalDomainAdd = (props) => { }, 'Close'), h('div', { - className: 'card__separator ' + className: 'card__separator' }), h('button', { diff --git a/src/ui/scripts/components/modals/ModalDomainEdit.js b/src/ui/scripts/components/modals/ModalDomainEdit.js index c5745361..162da123 100644 --- a/src/ui/scripts/components/modals/ModalDomainEdit.js +++ b/src/ui/scripts/components/modals/ModalDomainEdit.js @@ -103,7 +103,7 @@ const ModalDomainEdit = (props) => { }, 'Close'), h('div', { - className: 'card__separator ' + className: 'card__separator' }), h('button', { @@ -113,7 +113,7 @@ const ModalDomainEdit = (props) => { }, 'Delete'), h('div', { - className: 'card__separator ' + className: 'card__separator' }), h('button', { diff --git a/src/ui/scripts/components/modals/ModalEventEdit.js b/src/ui/scripts/components/modals/ModalEventEdit.js index 3db7952c..29c7f284 100644 --- a/src/ui/scripts/components/modals/ModalEventEdit.js +++ b/src/ui/scripts/components/modals/ModalEventEdit.js @@ -123,7 +123,7 @@ const ModalEventEdit = (props) => { }, 'Close'), h('div', { - className: 'card__separator ' + className: 'card__separator' }), h('button', { diff --git a/src/ui/scripts/components/overlays/OverlayFailure.js b/src/ui/scripts/components/overlays/OverlayFailure.js index f08f5bff..ba538a35 100644 --- a/src/ui/scripts/components/overlays/OverlayFailure.js +++ b/src/ui/scripts/components/overlays/OverlayFailure.js @@ -54,7 +54,7 @@ const OverlayFailure = (props) => { }, 'Help'), h('div', { - className: 'card__separator ' + className: 'card__separator' }), h('button', { diff --git a/src/ui/scripts/components/overlays/OverlayLogin.js b/src/ui/scripts/components/overlays/OverlayLogin.js index f8dd838f..ccec80dc 100644 --- a/src/ui/scripts/components/overlays/OverlayLogin.js +++ b/src/ui/scripts/components/overlays/OverlayLogin.js @@ -79,7 +79,7 @@ const OverlayLogin = (props) => { }, 'Help'), h('div', { - className: 'card__separator ' + className: 'card__separator' }), h('button', { From fb1d1197080f334b1aa6a1ef2795104e08b5b735 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 24 Oct 2020 17:42:10 +0200 Subject: [PATCH 013/208] Move filter types to misc --- src/types/miscellaneous.js | 49 ++++++++++++++++++++++++++++++++++++++ src/types/statistics.js | 49 -------------------------------------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/types/miscellaneous.js b/src/types/miscellaneous.js index 05865948..110d5672 100644 --- a/src/types/miscellaneous.js +++ b/src/types/miscellaneous.js @@ -4,4 +4,53 @@ const { gql } = require('apollo-server-micro') module.exports = gql` scalar URL + + enum Interval { + """ + Group by day. + """ + DAILY + """ + Group by month. + """ + MONTHLY + """ + Group by year. + """ + YEARLY + } + + enum Sorting { + """ + Entries with the most occurrences will be shown at the top. + """ + TOP + """ + Entries sorted by time. The newest entries will be shown at the top. + """ + RECENT + """ + Entries that appeared for the first time will be shown at the top. + """ + NEW + } + + enum Range { + """ + Data of the last 24 hours. + """ + LAST_24_HOURS + """ + Data of the last 7 days. + """ + LAST_7_DAYS + """ + Data of the last 30 days. + """ + LAST_30_DAYS + """ + Data of the last 6 months. + """ + LAST_6_MONTHS + } ` \ No newline at end of file diff --git a/src/types/statistics.js b/src/types/statistics.js index b39b9582..feac57ba 100644 --- a/src/types/statistics.js +++ b/src/types/statistics.js @@ -3,55 +3,6 @@ const { gql } = require('apollo-server-micro') module.exports = gql` - enum Interval { - """ - Group by day. - """ - DAILY - """ - Group by month. - """ - MONTHLY - """ - Group by year. - """ - YEARLY - } - - enum Sorting { - """ - Entries with the most occurrences will be shown at the top. - """ - TOP - """ - Entries sorted by time. The newest entries will be shown at the top. - """ - RECENT - """ - Entries that appeared for the first time will be shown at the top. - """ - NEW - } - - enum Range { - """ - Data of the last 24 hours. - """ - LAST_24_HOURS - """ - Data of the last 7 days. - """ - LAST_7_DAYS - """ - Data of the last 30 days. - """ - LAST_30_DAYS - """ - Data of the last 6 months. - """ - LAST_6_MONTHS - } - enum ViewType { """ Unique site views. From c9f032f004eb1256910f270b3ab6adaa973708cd Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 25 Oct 2020 13:59:48 +0100 Subject: [PATCH 014/208] Allow strings as referrer ids #173 --- src/types/statistics.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/statistics.js b/src/types/statistics.js index b39b9582..b4a090c4 100644 --- a/src/types/statistics.js +++ b/src/types/statistics.js @@ -91,9 +91,9 @@ module.exports = gql` type Referrer { """ - URL of the page. + Either the URL of the referrer or the ref, source or utm_source parameter of the page to indicate where the visit comes from. """ - id: URL! + id: String! """ Amount of occurrences. """ From 26fe09e80307ca4bccce4eb4700f2f695860e685 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 25 Oct 2020 14:04:37 +0100 Subject: [PATCH 015/208] Add source to schema --- src/types/records.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/types/records.js b/src/types/records.js index 0c831d86..b4403202 100644 --- a/src/types/records.js +++ b/src/types/records.js @@ -18,7 +18,11 @@ module.exports = gql` """ Where the user came from. Either unknown, a specific page or just the domain. This depends on the browser of the user. """ - siteReferrer: String + siteReferrer: URL + """ + Where the user came from. Either the ref, source or utm_source parameter. + """ + source: String """ Preferred language of the user. ISO 639-1 formatted. """ @@ -87,7 +91,7 @@ module.exports = gql` """ siteReferrer: URL """ - Where the user came from. + Where the user came from. Either the ref, source or utm_source parameter. """ source: String """ From 858c194a5029b16e4c39df5cdd7c64cb2899388f Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 25 Oct 2020 14:06:13 +0100 Subject: [PATCH 016/208] Improve readability --- src/aggregations/aggregateNewFields.js | 7 +++++-- src/aggregations/aggregateRecentFields.js | 7 +++++-- src/aggregations/aggregateTopFields.js | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/aggregations/aggregateNewFields.js b/src/aggregations/aggregateNewFields.js index a04fbcb3..03f077d4 100644 --- a/src/aggregations/aggregateNewFields.js +++ b/src/aggregations/aggregateNewFields.js @@ -28,8 +28,11 @@ module.exports = (ids, properties, limit, or) => { ] properties.forEach((property) => { - if (or) { - (aggregation[0].$match['$or'] = aggregation[0].$match['$or'] || []).push({ [property]: { $ne: null } }) + if (or === true) { + aggregation[0].$match['$or'] = [ + ...(aggregation[0].$match['$or'] || []), + { [property]: { $ne: null } } + ] } else { aggregation[0].$match[property] = { $ne: null } } diff --git a/src/aggregations/aggregateRecentFields.js b/src/aggregations/aggregateRecentFields.js index 00746656..4eb2f6cf 100644 --- a/src/aggregations/aggregateRecentFields.js +++ b/src/aggregations/aggregateRecentFields.js @@ -23,8 +23,11 @@ module.exports = (ids, properties, limit, or) => { ] properties.forEach((property) => { - if (or) { - (aggregation[0].$match['$or'] = aggregation[0].$match['$or'] || []).push({ [property]: { $ne: null } }) + if (or === true) { + aggregation[0].$match['$or'] = [ + ...(aggregation[0].$match['$or'] || []), + { [property]: { $ne: null } } + ] } else { aggregation[0].$match[property] = { $ne: null } } diff --git a/src/aggregations/aggregateTopFields.js b/src/aggregations/aggregateTopFields.js index 4427d196..46c935d5 100644 --- a/src/aggregations/aggregateTopFields.js +++ b/src/aggregations/aggregateTopFields.js @@ -26,8 +26,11 @@ module.exports = (ids, properties, range, limit, dateDetails, or) => { ] properties.forEach((property) => { - if (or) { - (aggregation[0].$match['$or'] = aggregation[0].$match['$or'] || []).push({ [property]: { $ne: null } }) + if (or === true) { + aggregation[0].$match['$or'] = [ + ...(aggregation[0].$match['$or'] || []), + { [property]: { $ne: null } } + ] } else { aggregation[0].$match[property] = { $ne: null } } From af4e0dbfcc37b96405b697c8c720e0dcfad54d0c Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 25 Oct 2020 15:10:59 +0100 Subject: [PATCH 017/208] Move shared code into lastFnByInterval function --- src/database/durations.js | 12 +----------- src/database/views.js | 12 +----------- src/utils/createDate.js | 17 ++++++++++++++++- 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/database/durations.js b/src/database/durations.js index c8cd4ccd..582c38fe 100644 --- a/src/database/durations.js +++ b/src/database/durations.js @@ -8,16 +8,6 @@ const intervals = require('../constants/intervals') const createArray = require('../utils/createArray') const matchesDate = require('../utils/matchesDate') -const includeFn = (dateDetails, interval) => { - - switch (interval) { - case intervals.INTERVALS_DAILY: return dateDetails.lastDays - case intervals.INTERVALS_MONTHLY: return dateDetails.lastMonths - case intervals.INTERVALS_YEARLY: return dateDetails.lastYears - } - -} - const get = async (ids, interval, limit, dateDetails) => { const enhance = (entries) => { @@ -28,7 +18,7 @@ const get = async (ids, interval, limit, dateDetails) => { return createArray(limit).map((_, index) => { - const date = includeFn(dateDetails, interval)(index) + const date = dateDetails.lastFnByInterval(interval)(index) // Views and durations are returning day, month and year in the // timezone of the user. We therefore need to match it against a diff --git a/src/database/views.js b/src/database/views.js index 3b45c94b..29bbf70d 100644 --- a/src/database/views.js +++ b/src/database/views.js @@ -9,16 +9,6 @@ const intervals = require('../constants/intervals') const createArray = require('../utils/createArray') const matchesDate = require('../utils/matchesDate') -const includeFn = (dateDetails, interval) => { - - switch (interval) { - case intervals.INTERVALS_DAILY: return dateDetails.lastDays - case intervals.INTERVALS_MONTHLY: return dateDetails.lastMonths - case intervals.INTERVALS_YEARLY: return dateDetails.lastYears - } - -} - const get = async (ids, type, interval, limit, dateDetails) => { const enhance = (entries) => { @@ -29,7 +19,7 @@ const get = async (ids, type, interval, limit, dateDetails) => { return createArray(limit).map((_, index) => { - const date = includeFn(dateDetails, interval)(index) + const date = dateDetails.lastFnByInterval(interval)(index) // Views and durations are returning day, month and year in the // timezone of the user. We therefore need to match it against a diff --git a/src/utils/createDate.js b/src/utils/createDate.js index f8eb996c..3af70555 100644 --- a/src/utils/createDate.js +++ b/src/utils/createDate.js @@ -2,6 +2,7 @@ const { subMilliseconds, subHours, subDays, subMonths, subYears, startOfDay, startOfMonth, startOfYear } = require('date-fns') const serverTimeZone = require('./timeZone') +const intervals = require('../constants/intervals') module.exports = (userTimeZone = serverTimeZone) => { @@ -18,7 +19,7 @@ module.exports = (userTimeZone = serverTimeZone) => { // things more complicated. The max offset does the job. const timeZoneToleranz = 14 - return { + const instance = { userTimeZone, // Get a date with an offset lastMilliseconds: (milliseconds) => subMilliseconds(currentDate, milliseconds), @@ -32,4 +33,18 @@ module.exports = (userTimeZone = serverTimeZone) => { includeYears: (years) => subHours(subYears(startOfYear(currentDate), years - 1), timeZoneToleranz) } + // Get the function that matches the interval + const lastFnByInterval = (interval) => { + switch (interval) { + case intervals.INTERVALS_DAILY: return instance.lastDays + case intervals.INTERVALS_MONTHLY: return instance.lastMonths + case intervals.INTERVALS_YEARLY: return instance.lastYears + } + } + + return { + ...instance, + lastFnByInterval + } + } \ No newline at end of file From 5c5b1fddacaea68f675e279434ef409c7480c293 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 25 Oct 2020 15:20:40 +0100 Subject: [PATCH 018/208] Adjust comment --- src/database/durations.js | 2 +- src/database/views.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/durations.js b/src/database/durations.js index 582c38fe..249ba5f9 100644 --- a/src/database/durations.js +++ b/src/database/durations.js @@ -20,7 +20,7 @@ const get = async (ids, interval, limit, dateDetails) => { const date = dateDetails.lastFnByInterval(interval)(index) - // Views and durations are returning day, month and year in the + // Database entries include the day, month and year in the // timezone of the user. We therefore need to match it against a // date in the timezone of the user. const userZonedDate = utcToZonedTime(date, dateDetails.userTimeZone) diff --git a/src/database/views.js b/src/database/views.js index 29bbf70d..724d2741 100644 --- a/src/database/views.js +++ b/src/database/views.js @@ -21,7 +21,7 @@ const get = async (ids, type, interval, limit, dateDetails) => { const date = dateDetails.lastFnByInterval(interval)(index) - // Views and durations are returning day, month and year in the + // Database entries include the day, month and year in the // timezone of the user. We therefore need to match it against a // date in the timezone of the user. const userZonedDate = utcToZonedTime(date, dateDetails.userTimeZone) From 4a073bb9e16854fc4e0f0b6ca0cdefd946e200a5 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 25 Oct 2020 15:30:03 +0100 Subject: [PATCH 019/208] Adjust chart aggregations --- src/aggregations/aggregateDurations.js | 25 ++++++++----------------- src/aggregations/aggregateViews.js | 26 ++++++++------------------ src/utils/createDate.js | 14 ++++++++++++-- 3 files changed, 28 insertions(+), 37 deletions(-) diff --git a/src/aggregations/aggregateDurations.js b/src/aggregations/aggregateDurations.js index 7a8ff935..2c9578a3 100644 --- a/src/aggregations/aggregateDurations.js +++ b/src/aggregations/aggregateDurations.js @@ -23,25 +23,16 @@ module.exports = (ids, interval, limit, dateDetails) => { } ] + aggregation[0].$match.created = { $gte: dateDetails.includeFnByInterval(interval)(limit) } + const dateExpression = { date: '$created', timezone: dateDetails.userTimeZone } + const matchDay = [ intervals.INTERVALS_DAILY ].includes(interval) + const matchMonth = [ intervals.INTERVALS_DAILY, intervals.INTERVALS_MONTHLY ].includes(interval) + const matchYear = [ intervals.INTERVALS_DAILY, intervals.INTERVALS_MONTHLY, intervals.INTERVALS_YEARLY ].includes(interval) - if (interval === intervals.INTERVALS_DAILY) { - aggregation[0].$match.created = { $gte: dateDetails.includeDays(limit) } - aggregation[4].$group._id.day = { $dayOfMonth: dateExpression } - aggregation[4].$group._id.month = { $month: dateExpression } - aggregation[4].$group._id.year = { $year: dateExpression } - } - - if (interval === intervals.INTERVALS_MONTHLY) { - aggregation[0].$match.created = { $gte: dateDetails.includeMonths(limit) } - aggregation[4].$group._id.month = { $month: dateExpression } - aggregation[4].$group._id.year = { $year: dateExpression } - } - - if (interval === intervals.INTERVALS_YEARLY) { - aggregation[0].$match.created = { $gte: dateDetails.includeYears(limit) } - aggregation[4].$group._id.year = { $year: dateExpression } - } + if (matchDay === true) aggregation[4].$group._id.day = { $dayOfMonth: dateExpression } + if (matchMonth === true) aggregation[4].$group._id.month = { $month: dateExpression } + if (matchYear === true) aggregation[4].$group._id.year = { $year: dateExpression } return aggregation diff --git a/src/aggregations/aggregateViews.js b/src/aggregations/aggregateViews.js index 99e86dd5..c02d5158 100644 --- a/src/aggregations/aggregateViews.js +++ b/src/aggregations/aggregateViews.js @@ -17,31 +17,21 @@ module.exports = (ids, unique, interval, limit, dateDetails) => { } ] - if (unique === true) aggregation[0].$match.clientId = { $exists: true, $ne: null } - const dateExpression = { date: '$created', timezone: dateDetails.userTimeZone } - - if (interval === intervals.INTERVALS_DAILY) { - aggregation[0].$match.created = { $gte: dateDetails.includeDays(limit) } - aggregation[1].$group._id.day = { $dayOfMonth: dateExpression } - aggregation[1].$group._id.month = { $month: dateExpression } - aggregation[1].$group._id.year = { $year: dateExpression } - } + aggregation[0].$match.created = { $gte: dateDetails.includeFnByInterval(interval)(limit) } - if (interval === intervals.INTERVALS_MONTHLY) { - aggregation[0].$match.created = { $gte: dateDetails.includeMonths(limit) } - aggregation[1].$group._id.month = { $month: dateExpression } - aggregation[1].$group._id.year = { $year: dateExpression } - } + const dateExpression = { date: '$created', timezone: dateDetails.userTimeZone } + const matchDay = [ intervals.INTERVALS_DAILY ].includes(interval) + const matchMonth = [ intervals.INTERVALS_DAILY, intervals.INTERVALS_MONTHLY ].includes(interval) + const matchYear = [ intervals.INTERVALS_DAILY, intervals.INTERVALS_MONTHLY, intervals.INTERVALS_YEARLY ].includes(interval) - if (interval === intervals.INTERVALS_YEARLY) { - aggregation[0].$match.created = { $gte: dateDetails.includeYears(limit) } - aggregation[1].$group._id.year = { $year: dateExpression } - } + if (matchDay === true) aggregation[1].$group._id.day = { $dayOfMonth: dateExpression } + if (matchMonth === true) aggregation[1].$group._id.month = { $month: dateExpression } + if (matchYear === true) aggregation[1].$group._id.year = { $year: dateExpression } return aggregation diff --git a/src/utils/createDate.js b/src/utils/createDate.js index 3af70555..f248660f 100644 --- a/src/utils/createDate.js +++ b/src/utils/createDate.js @@ -33,7 +33,7 @@ module.exports = (userTimeZone = serverTimeZone) => { includeYears: (years) => subHours(subYears(startOfYear(currentDate), years - 1), timeZoneToleranz) } - // Get the function that matches the interval + // Get the last-function that matches the interval const lastFnByInterval = (interval) => { switch (interval) { case intervals.INTERVALS_DAILY: return instance.lastDays @@ -42,9 +42,19 @@ module.exports = (userTimeZone = serverTimeZone) => { } } + // Get the include-function that matches the interval + const includeFnByInterval = (interval) => { + switch (interval) { + case intervals.INTERVALS_DAILY: return instance.includeDays + case intervals.INTERVALS_MONTHLY: return instance.includeMonths + case intervals.INTERVALS_YEARLY: return instance.includeYears + } + } + return { ...instance, - lastFnByInterval + lastFnByInterval, + includeFnByInterval } } \ No newline at end of file From aa00174cea60012a8f96cb2bdc8e29cf7ed262e9 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 25 Oct 2020 16:29:47 +0100 Subject: [PATCH 020/208] Fix variable name --- src/resolvers/actions.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/resolvers/actions.js b/src/resolvers/actions.js index 2cec10d3..3788ff55 100644 --- a/src/resolvers/actions.js +++ b/src/resolvers/actions.js @@ -22,11 +22,11 @@ const polish = (obj) => { module.exports = { Mutation: { - createAction: async (parent, { recordId, input }) => { + createAction: async (parent, { eventId, input }) => { - const data = polish({ ...input, recordId }) + const data = polish({ ...input, eventId }) - const event = await events.get(recordId) + const event = await events.get(eventId) if (event == null) throw new KnownError('Unknown event') From 314a6c0f50f90eb60cf21820906a40fa1a53df9b Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 25 Oct 2020 16:30:16 +0100 Subject: [PATCH 021/208] Aggregations for events and more --- src/aggregations/aggregateActions.js | 33 +++++++ src/aggregations/aggregateNewActions.js | 35 ++++++++ src/aggregations/aggregateRecentActions.js | 30 +++++++ src/aggregations/aggregateTopActions.js | 49 +++++++++++ src/database/actions.js | 86 +++++++++++++++++++ .../{statistics.js => domainStatistics.js} | 2 +- src/resolvers/eventStatistics.js | 22 +++++ src/resolvers/events.js | 3 + src/resolvers/index.js | 5 +- src/stages/matchEvents.js | 17 ++++ .../{statistics.js => domainStatistics.js} | 4 +- src/types/domains.js | 2 +- src/types/eventStatistics.js | 58 +++++++++++++ src/types/events.js | 4 + src/types/index.js | 7 +- 15 files changed, 348 insertions(+), 9 deletions(-) create mode 100644 src/aggregations/aggregateActions.js create mode 100644 src/aggregations/aggregateNewActions.js create mode 100644 src/aggregations/aggregateRecentActions.js create mode 100644 src/aggregations/aggregateTopActions.js rename src/resolvers/{statistics.js => domainStatistics.js} (99%) create mode 100644 src/resolvers/eventStatistics.js create mode 100644 src/stages/matchEvents.js rename src/types/{statistics.js => domainStatistics.js} (98%) create mode 100644 src/types/eventStatistics.js diff --git a/src/aggregations/aggregateActions.js b/src/aggregations/aggregateActions.js new file mode 100644 index 00000000..92d545d7 --- /dev/null +++ b/src/aggregations/aggregateActions.js @@ -0,0 +1,33 @@ +'use strict' + +const intervals = require('../constants/intervals') +const matchEvents = require('../stages/matchEvents') + +module.exports = (ids, interval, limit, dateDetails) => { + + const aggregation = [ + matchEvents(ids), + { + $group: { + _id: {}, + count: { + $sum: '$value' + } + } + } + ] + + aggregation[0].$match.created = { $gte: dateDetails.includeFnByInterval(interval)(limit) } + + const dateExpression = { date: '$created', timezone: dateDetails.userTimeZone } + const matchDay = [ intervals.INTERVALS_DAILY ].includes(interval) + const matchMonth = [ intervals.INTERVALS_DAILY, intervals.INTERVALS_MONTHLY ].includes(interval) + const matchYear = [ intervals.INTERVALS_DAILY, intervals.INTERVALS_MONTHLY, intervals.INTERVALS_YEARLY ].includes(interval) + + if (matchDay === true) aggregation[1].$group._id.day = { $dayOfMonth: dateExpression } + if (matchMonth === true) aggregation[1].$group._id.month = { $month: dateExpression } + if (matchYear === true) aggregation[1].$group._id.year = { $year: dateExpression } + + return aggregation + +} \ No newline at end of file diff --git a/src/aggregations/aggregateNewActions.js b/src/aggregations/aggregateNewActions.js new file mode 100644 index 00000000..66227049 --- /dev/null +++ b/src/aggregations/aggregateNewActions.js @@ -0,0 +1,35 @@ +'use strict' + +const matchEvents = require('../stages/matchEvents') + +module.exports = (ids, limit) => { + + const aggregation = [ + matchEvents(ids), + { + $group: { + _id: {}, + count: { + $sum: '$value' + }, + created: { + $first: '$created' + } + } + }, + { + $sort: { + created: -1 + } + }, + { + $limit: limit + } + ] + + aggregation[0].$match.key = { $ne: null } + aggregation[1].$group._id.key = '$key' + + return aggregation + +} \ No newline at end of file diff --git a/src/aggregations/aggregateRecentActions.js b/src/aggregations/aggregateRecentActions.js new file mode 100644 index 00000000..acd18f3a --- /dev/null +++ b/src/aggregations/aggregateRecentActions.js @@ -0,0 +1,30 @@ +'use strict' + +const matchEvents = require('../stages/matchEvents') + +module.exports = (ids, limit) => { + + const aggregation = [ + matchEvents(ids), + { + $sort: { + created: -1 + } + }, + { + $project: { + _id: {}, + created: '$created' + } + }, + { + $limit: limit + } + ] + + aggregation[0].$match.key = { $ne: null } + aggregation[2].$group._id.key = '$key' + + return aggregation + +} \ No newline at end of file diff --git a/src/aggregations/aggregateTopActions.js b/src/aggregations/aggregateTopActions.js new file mode 100644 index 00000000..d790a9be --- /dev/null +++ b/src/aggregations/aggregateTopActions.js @@ -0,0 +1,49 @@ +'use strict' + +const ranges = require('../constants/ranges') +const matchEvents = require('../stages/matchEvents') + +module.exports = (ids, range, limit, dateDetails) => { + + const aggregation = [ + matchEvents(ids), + { + $group: { + _id: {}, + count: { + $sum: '$value' + } + } + }, + { + $sort: { + count: -1 + } + }, + { + $limit: limit + } + ] + + aggregation[0].$match.key = { $ne: null } + aggregation[1].$group._id.key = '$key' + + if (range === ranges.RANGES_LAST_24_HOURS) { + aggregation[0].$match.created = { $gte: dateDetails.lastHours(24) } + } + + if (range === ranges.RANGES_LAST_7_DAYS) { + aggregation[0].$match.created = { $gte: dateDetails.lastDays(7) } + } + + if (range === ranges.RANGES_LAST_30_DAYS) { + aggregation[0].$match.created = { $gte: dateDetails.lastDays(30) } + } + + if (range === ranges.RANGES_LAST_6_MONTHS) { + aggregation[0].$match.created = { $gte: dateDetails.lastMonths(6) } + } + + return aggregation + +} \ No newline at end of file diff --git a/src/database/actions.js b/src/database/actions.js index b1f20f67..4e90a1cd 100644 --- a/src/database/actions.js +++ b/src/database/actions.js @@ -1,6 +1,16 @@ 'use strict' +const { utcToZonedTime } = require('date-fns-tz') + const Action = require('../models/Action') +const aggregateTopActions = require('../aggregations/aggregateTopActions') +const aggregateNewActions = require('../aggregations/aggregateNewActions') +const aggregateRecentActions = require('../aggregations/aggregateRecentActions') +const aggregateActions = require('../aggregations/aggregateActions') +const sortings = require('../constants/sortings') +const intervals = require('../constants/intervals') +const createArray = require('../utils/createArray') +const matchesDate = require('../utils/matchesDate') const response = (entry) => ({ id: entry.id, @@ -46,6 +56,80 @@ const update = async (id, data) => { } +const getChart = async (ids, interval, limit, dateDetails) => { + + const enhance = (entries) => { + + const matchDay = [ intervals.INTERVALS_DAILY ].includes(interval) + const matchMonth = [ intervals.INTERVALS_DAILY, intervals.INTERVALS_MONTHLY ].includes(interval) + const matchYear = [ intervals.INTERVALS_DAILY, intervals.INTERVALS_MONTHLY, intervals.INTERVALS_YEARLY ].includes(interval) + + return createArray(limit).map((_, index) => { + + const date = dateDetails.lastFnByInterval(interval)(index) + + // Database entries include the day, month and year in the + // timezone of the user. We therefore need to match it against a + // date in the timezone of the user. + const userZonedDate = utcToZonedTime(date, dateDetails.userTimeZone) + + // Find a entry that matches the date + const entry = entries.find((entry) => { + return matchesDate( + matchDay === true ? entry._id.day : undefined, + matchMonth === true ? entry._id.month : undefined, + matchYear === true ? entry._id.year : undefined, + userZonedDate + ) + }) + + return { + id: date, + count: entry == null ? 0 : entry.count + } + + }) + + } + + const aggregation = (() => { + + return aggregateActions(ids, interval, limit, dateDetails) + + })() + + return enhance( + await Action.aggregate(aggregation) + ) + +} + +const getList = async (ids, sorting, range, limit, dateDetails) => { + + const enhance = (entries) => { + + return entries.map((entry) => ({ + id: entry._id.key, + count: entry.count, + created: entry.created + })) + + } + + const aggregation = (() => { + + if (sorting === sortings.SORTINGS_TOP) return aggregateTopActions(ids, range, limit, dateDetails) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewActions(ids, limit) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentActions(ids, limit) + + })() + + return enhance( + await Action.aggregate(aggregation) + ) + +} + const del = async (eventId) => { return Action.deleteMany({ @@ -57,5 +141,7 @@ const del = async (eventId) => { module.exports = { add, update, + getChart, + getList, del } \ No newline at end of file diff --git a/src/resolvers/statistics.js b/src/resolvers/domainStatistics.js similarity index 99% rename from src/resolvers/statistics.js rename to src/resolvers/domainStatistics.js index e389c60b..d606663c 100644 --- a/src/resolvers/statistics.js +++ b/src/resolvers/domainStatistics.js @@ -14,7 +14,7 @@ const domainIds = require('../utils/domainIds') const requireAuth = require('../middlewares/requireAuth') module.exports = { - Statistics: { + DomainStatistics: { views: pipe(requireAuth, async (domain, { type, interval, limit }, { dateDetails }) => { const ids = await domainIds(domain) diff --git a/src/resolvers/eventStatistics.js b/src/resolvers/eventStatistics.js new file mode 100644 index 00000000..387b197b --- /dev/null +++ b/src/resolvers/eventStatistics.js @@ -0,0 +1,22 @@ +'use strict' + +const actions = require('../database/actions') +const pipe = require('../utils/pipe') +const requireAuth = require('../middlewares/requireAuth') + +module.exports = { + EventStatistics: { + chart: pipe(requireAuth, async (event, { interval, limit }, { dateDetails }) => { + + const ids = [ event.id ] + return actions.getChart(ids, interval, limit, dateDetails) + + }), + list: pipe(requireAuth, async (event, { sorting, range, limit }, { dateDetails }) => { + + const ids = [ event.id ] + return actions.getList(ids, sorting, range, limit, dateDetails) + + }) + } +} \ No newline at end of file diff --git a/src/resolvers/events.js b/src/resolvers/events.js index bdf90989..4d3b4da5 100644 --- a/src/resolvers/events.js +++ b/src/resolvers/events.js @@ -9,6 +9,9 @@ const requireAuth = require('../middlewares/requireAuth') const blockDemoMode = require('../middlewares/blockDemoMode') module.exports = { + Event: { + statistics: (obj) => obj + }, Query: { event: pipe(requireAuth, async (parent, { id }) => { diff --git a/src/resolvers/index.js b/src/resolvers/index.js index 39839169..d2259b0d 100644 --- a/src/resolvers/index.js +++ b/src/resolvers/index.js @@ -6,8 +6,9 @@ module.exports = mergeResolvers([ require('./tokens'), require('./records'), require('./domains'), - require('./actions'), require('./events'), + require('./actions'), require('./facts'), - require('./statistics') + require('./domainStatistics'), + require('./eventStatistics') ]) \ No newline at end of file diff --git a/src/stages/matchEvents.js b/src/stages/matchEvents.js new file mode 100644 index 00000000..6a9aa601 --- /dev/null +++ b/src/stages/matchEvents.js @@ -0,0 +1,17 @@ +'use strict' + +module.exports = (ids) => { + + const stage = { + $match: {} + } + + if (ids != null) { + stage.$match.eventId = { + $in: ids + } + } + + return stage + +} \ No newline at end of file diff --git a/src/types/statistics.js b/src/types/domainStatistics.js similarity index 98% rename from src/types/statistics.js rename to src/types/domainStatistics.js index feac57ba..7cb16fe8 100644 --- a/src/types/statistics.js +++ b/src/types/domainStatistics.js @@ -204,7 +204,7 @@ module.exports = gql` """ Statistics of a domain. Usually data that needs to be represented in a list or chart. """ - type Statistics { + type DomainStatistics { """ Amount of views grouped by day, month or year. """ @@ -313,6 +313,6 @@ module.exports = gql` """ Statistics of all domains combined. Usually data that needs to be represented in a list or chart. """ - statistics: Statistics! + statistics: DomainStatistics! } ` \ No newline at end of file diff --git a/src/types/domains.js b/src/types/domains.js index 92fb8f1c..a8b7ec81 100644 --- a/src/types/domains.js +++ b/src/types/domains.js @@ -22,7 +22,7 @@ module.exports = gql` """ Statistics of a domain. Usually data that needs to be represented in a list or chart. """ - statistics: Statistics! + statistics: DomainStatistics! """ Identifies the date and time when the object was created. """ diff --git a/src/types/eventStatistics.js b/src/types/eventStatistics.js new file mode 100644 index 00000000..040239e6 --- /dev/null +++ b/src/types/eventStatistics.js @@ -0,0 +1,58 @@ +'use strict' + +const { gql } = require('apollo-server-micro') + +module.exports = gql` + type EventChartEntry { + """ + Date of the event. + """ + id: DateTime! + """ + Sum of values on that date. + """ + count: Float! + } + + type EventListEntry { + """ + Key of the event. + """ + id: String! + """ + Sum of values of the current event key. + """ + count: Float + """ + Identifies the date and time when the object was created. + """ + created: DateTime + } + + """ + Statistics of an event. The data is available in different types, depending on whether they are to be shown in a chart or list. + """ + type EventStatistics { + """ + The chart type should be used when showing events in a chart. It groups events by an interval and shows the sum of values on each entry. + """ + chart( + interval: Interval!, + """ + Number of entries to return. Starts with the current day, month or year depending on the chosen interval. + """ + limit: Int = 14 + ): [EventChartEntry!] + """ + The list type should be used when showing events in a list. It groups events by their key and shows the sum of values on each entry. + """ + list( + sorting: Sorting!, + range: Range = LAST_7_DAYS, + """ + Number of entries to return. + """ + limit: Int = 30 + ): [EventListEntry!] + } +` \ No newline at end of file diff --git a/src/types/events.js b/src/types/events.js index d8821a95..069a74cd 100644 --- a/src/types/events.js +++ b/src/types/events.js @@ -31,6 +31,10 @@ module.exports = gql` """ type: EventType! """ + Statistics of an event. The data is available in different types, depending on whether they are to be shown in a chart or list. + """ + statistics: EventStatistics! + """ Identifies the date and time when the object was created. """ created: DateTime! diff --git a/src/types/index.js b/src/types/index.js index 7af2ac21..14954581 100644 --- a/src/types/index.js +++ b/src/types/index.js @@ -3,12 +3,13 @@ const { mergeTypeDefs } = require('graphql-tools') module.exports = mergeTypeDefs([ + require('./tokens'), + require('./records'), require('./domains'), require('./events'), require('./actions'), require('./facts'), require('./miscellaneous'), - require('./records'), - require('./statistics'), - require('./tokens') + require('./domainStatistics'), + require('./eventStatistics') ], { all: true }) \ No newline at end of file From a917d1dac7725a2e4c6721ca87d83368415a466a Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 25 Oct 2020 16:34:35 +0100 Subject: [PATCH 022/208] Rename aggregations --- ...ateNewFields.js => aggregateNewRecords.js} | 0 ...entFields.js => aggregateRecentRecords.js} | 0 ...ateTopFields.js => aggregateTopRecords.js} | 0 src/database/browsers.js | 18 ++++---- src/database/devices.js | 18 ++++---- src/database/languages.js | 12 +++--- src/database/pages.js | 12 +++--- src/database/referrers.js | 12 +++--- src/database/sizes.js | 42 +++++++++---------- src/database/systems.js | 18 ++++---- test/aggregations/aggregateNewFields.js | 4 +- test/aggregations/aggregateRecentFields.js | 4 +- test/aggregations/aggregateTopFields.js | 4 +- 13 files changed, 72 insertions(+), 72 deletions(-) rename src/aggregations/{aggregateNewFields.js => aggregateNewRecords.js} (100%) rename src/aggregations/{aggregateRecentFields.js => aggregateRecentRecords.js} (100%) rename src/aggregations/{aggregateTopFields.js => aggregateTopRecords.js} (100%) diff --git a/src/aggregations/aggregateNewFields.js b/src/aggregations/aggregateNewRecords.js similarity index 100% rename from src/aggregations/aggregateNewFields.js rename to src/aggregations/aggregateNewRecords.js diff --git a/src/aggregations/aggregateRecentFields.js b/src/aggregations/aggregateRecentRecords.js similarity index 100% rename from src/aggregations/aggregateRecentFields.js rename to src/aggregations/aggregateRecentRecords.js diff --git a/src/aggregations/aggregateTopFields.js b/src/aggregations/aggregateTopRecords.js similarity index 100% rename from src/aggregations/aggregateTopFields.js rename to src/aggregations/aggregateTopRecords.js diff --git a/src/database/browsers.js b/src/database/browsers.js index c2c36faf..e4516797 100644 --- a/src/database/browsers.js +++ b/src/database/browsers.js @@ -1,9 +1,9 @@ 'use strict' const Record = require('../models/Record') -const aggregateTopFields = require('../aggregations/aggregateTopFields') -const aggregateNewFields = require('../aggregations/aggregateNewFields') -const aggregateRecentFields = require('../aggregations/aggregateRecentFields') +const aggregateTopRecords = require('../aggregations/aggregateTopRecords') +const aggregateNewRecords = require('../aggregations/aggregateNewRecords') +const aggregateRecentRecords = require('../aggregations/aggregateRecentRecords') const sortings = require('../constants/sortings') const constants = require('../constants/browsers') const bestMatch = require('../utils/bestMatch') @@ -26,14 +26,14 @@ const get = async (ids, sorting, type, range, limit, dateDetails) => { const aggregation = (() => { if (type === constants.BROWSERS_TYPE_NO_VERSION) { - if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'browserName' ], range, limit, dateDetails) - if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'browserName' ], limit) - if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'browserName' ], limit) + if (sorting === sortings.SORTINGS_TOP) return aggregateTopRecords(ids, [ 'browserName' ], range, limit, dateDetails) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewRecords(ids, [ 'browserName' ], limit) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentRecords(ids, [ 'browserName' ], limit) } if (type === constants.BROWSERS_TYPE_WITH_VERSION) { - if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'browserName', 'browserVersion' ], range, limit, dateDetails) - if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'browserName', 'browserVersion' ], limit) - if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'browserName', 'browserVersion' ], limit) + if (sorting === sortings.SORTINGS_TOP) return aggregateTopRecords(ids, [ 'browserName', 'browserVersion' ], range, limit, dateDetails) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewRecords(ids, [ 'browserName', 'browserVersion' ], limit) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentRecords(ids, [ 'browserName', 'browserVersion' ], limit) } })() diff --git a/src/database/devices.js b/src/database/devices.js index fab9ffd8..7b467749 100644 --- a/src/database/devices.js +++ b/src/database/devices.js @@ -1,9 +1,9 @@ 'use strict' const Record = require('../models/Record') -const aggregateTopFields = require('../aggregations/aggregateTopFields') -const aggregateNewFields = require('../aggregations/aggregateNewFields') -const aggregateRecentFields = require('../aggregations/aggregateRecentFields') +const aggregateTopRecords = require('../aggregations/aggregateTopRecords') +const aggregateNewRecords = require('../aggregations/aggregateNewRecords') +const aggregateRecentRecords = require('../aggregations/aggregateRecentRecords') const sortings = require('../constants/sortings') const constants = require('../constants/devices') const bestMatch = require('../utils/bestMatch') @@ -26,14 +26,14 @@ const get = async (ids, sorting, type, range, limit, dateDetails) => { const aggregation = (() => { if (type === constants.DEVICES_TYPE_NO_MODEL) { - if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'deviceManufacturer' ], range, limit, dateDetails) - if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'deviceManufacturer' ], limit) - if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'deviceManufacturer' ], limit) + if (sorting === sortings.SORTINGS_TOP) return aggregateTopRecords(ids, [ 'deviceManufacturer' ], range, limit, dateDetails) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewRecords(ids, [ 'deviceManufacturer' ], limit) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentRecords(ids, [ 'deviceManufacturer' ], limit) } if (type === constants.DEVICES_TYPE_WITH_MODEL) { - if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'deviceManufacturer', 'deviceName' ], range, limit, dateDetails) - if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'deviceManufacturer', 'deviceName' ], limit) - if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'deviceManufacturer', 'deviceName' ], limit) + if (sorting === sortings.SORTINGS_TOP) return aggregateTopRecords(ids, [ 'deviceManufacturer', 'deviceName' ], range, limit, dateDetails) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewRecords(ids, [ 'deviceManufacturer', 'deviceName' ], limit) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentRecords(ids, [ 'deviceManufacturer', 'deviceName' ], limit) } })() diff --git a/src/database/languages.js b/src/database/languages.js index d8c89e86..0a79e6a5 100644 --- a/src/database/languages.js +++ b/src/database/languages.js @@ -1,9 +1,9 @@ 'use strict' const Record = require('../models/Record') -const aggregateTopFields = require('../aggregations/aggregateTopFields') -const aggregateNewFields = require('../aggregations/aggregateNewFields') -const aggregateRecentFields = require('../aggregations/aggregateRecentFields') +const aggregateTopRecords = require('../aggregations/aggregateTopRecords') +const aggregateNewRecords = require('../aggregations/aggregateNewRecords') +const aggregateRecentRecords = require('../aggregations/aggregateRecentRecords') const sortings = require('../constants/sortings') const languageCodes = require('../utils/languageCodes') @@ -21,9 +21,9 @@ const get = async (ids, sorting, range, limit, dateDetails) => { const aggregation = (() => { - if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'siteLanguage' ], range, limit, dateDetails) - if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'siteLanguage' ], limit) - if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'siteLanguage' ], limit) + if (sorting === sortings.SORTINGS_TOP) return aggregateTopRecords(ids, [ 'siteLanguage' ], range, limit, dateDetails) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewRecords(ids, [ 'siteLanguage' ], limit) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentRecords(ids, [ 'siteLanguage' ], limit) })() diff --git a/src/database/pages.js b/src/database/pages.js index 52292c28..80abd92d 100644 --- a/src/database/pages.js +++ b/src/database/pages.js @@ -1,9 +1,9 @@ 'use strict' const Record = require('../models/Record') -const aggregateTopFields = require('../aggregations/aggregateTopFields') -const aggregateNewFields = require('../aggregations/aggregateNewFields') -const aggregateRecentFields = require('../aggregations/aggregateRecentFields') +const aggregateTopRecords = require('../aggregations/aggregateTopRecords') +const aggregateNewRecords = require('../aggregations/aggregateNewRecords') +const aggregateRecentRecords = require('../aggregations/aggregateRecentRecords') const sortings = require('../constants/sortings') const get = async (ids, sorting, range, limit, dateDetails) => { @@ -20,9 +20,9 @@ const get = async (ids, sorting, range, limit, dateDetails) => { const aggregation = (() => { - if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'siteLocation' ], range, limit, dateDetails) - if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'siteLocation' ], limit) - if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'siteLocation' ], limit) + if (sorting === sortings.SORTINGS_TOP) return aggregateTopRecords(ids, [ 'siteLocation' ], range, limit, dateDetails) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewRecords(ids, [ 'siteLocation' ], limit) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentRecords(ids, [ 'siteLocation' ], limit) })() diff --git a/src/database/referrers.js b/src/database/referrers.js index d0c365e2..2c450e7d 100644 --- a/src/database/referrers.js +++ b/src/database/referrers.js @@ -1,9 +1,9 @@ 'use strict' const Record = require('../models/Record') -const aggregateTopFields = require('../aggregations/aggregateTopFields') -const aggregateRecentFields = require('../aggregations/aggregateRecentFields') -const aggregateNewFields = require('../aggregations/aggregateNewFields') +const aggregateTopRecords = require('../aggregations/aggregateTopRecords') +const aggregateRecentRecords = require('../aggregations/aggregateRecentRecords') +const aggregateNewRecords = require('../aggregations/aggregateNewRecords') const sortings = require('../constants/sortings') const get = async (ids, sorting, range, limit, dateDetails) => { @@ -20,9 +20,9 @@ const get = async (ids, sorting, range, limit, dateDetails) => { const aggregation = (() => { - if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'siteReferrer' ], range, limit, dateDetails) - if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'siteReferrer' ], limit) - if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'siteReferrer' ], limit) + if (sorting === sortings.SORTINGS_TOP) return aggregateTopRecords(ids, [ 'siteReferrer' ], range, limit, dateDetails) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewRecords(ids, [ 'siteReferrer' ], limit) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentRecords(ids, [ 'siteReferrer' ], limit) })() diff --git a/src/database/sizes.js b/src/database/sizes.js index 06c03ccf..0321214a 100644 --- a/src/database/sizes.js +++ b/src/database/sizes.js @@ -1,9 +1,9 @@ 'use strict' const Record = require('../models/Record') -const aggregateTopFields = require('../aggregations/aggregateTopFields') -const aggregateNewFields = require('../aggregations/aggregateNewFields') -const aggregateRecentFields = require('../aggregations/aggregateRecentFields') +const aggregateTopRecords = require('../aggregations/aggregateTopRecords') +const aggregateNewRecords = require('../aggregations/aggregateNewRecords') +const aggregateRecentRecords = require('../aggregations/aggregateRecentRecords') const sortings = require('../constants/sortings') const constants = require('../constants/sizes') const bestMatch = require('../utils/bestMatch') @@ -30,28 +30,28 @@ const get = async (ids, sorting, type, range, limit, dateDetails) => { const aggregation = (() => { if (sorting === sortings.SORTINGS_TOP) { - if (type === constants.SIZES_TYPE_BROWSER_WIDTH) return aggregateTopFields(ids, [ 'browserWidth' ], range, limit, dateDetails) - if (type === constants.SIZES_TYPE_BROWSER_HEIGHT) return aggregateTopFields(ids, [ 'browserHeight' ], range, limit, dateDetails) - if (type === constants.SIZES_TYPE_BROWSER_RESOLUTION) return aggregateTopFields(ids, [ 'browserWidth', 'browserHeight' ], range, limit, dateDetails) - if (type === constants.SIZES_TYPE_SCREEN_WIDTH) return aggregateTopFields(ids, [ 'screenWidth' ], range, limit, dateDetails) - if (type === constants.SIZES_TYPE_SCREEN_HEIGHT) return aggregateTopFields(ids, [ 'screenHeight' ], range, limit, dateDetails) - if (type === constants.SIZES_TYPE_SCREEN_RESOLUTION) return aggregateTopFields(ids, [ 'screenWidth', 'screenHeight' ], range, limit, dateDetails) + if (type === constants.SIZES_TYPE_BROWSER_WIDTH) return aggregateTopRecords(ids, [ 'browserWidth' ], range, limit, dateDetails) + if (type === constants.SIZES_TYPE_BROWSER_HEIGHT) return aggregateTopRecords(ids, [ 'browserHeight' ], range, limit, dateDetails) + if (type === constants.SIZES_TYPE_BROWSER_RESOLUTION) return aggregateTopRecords(ids, [ 'browserWidth', 'browserHeight' ], range, limit, dateDetails) + if (type === constants.SIZES_TYPE_SCREEN_WIDTH) return aggregateTopRecords(ids, [ 'screenWidth' ], range, limit, dateDetails) + if (type === constants.SIZES_TYPE_SCREEN_HEIGHT) return aggregateTopRecords(ids, [ 'screenHeight' ], range, limit, dateDetails) + if (type === constants.SIZES_TYPE_SCREEN_RESOLUTION) return aggregateTopRecords(ids, [ 'screenWidth', 'screenHeight' ], range, limit, dateDetails) } if (sorting === sortings.SORTINGS_NEW) { - if (type === constants.SIZES_TYPE_BROWSER_WIDTH) return aggregateNewFields(ids, [ 'browserWidth' ], limit) - if (type === constants.SIZES_TYPE_BROWSER_HEIGHT) return aggregateNewFields(ids, [ 'browserHeight' ], limit) - if (type === constants.SIZES_TYPE_BROWSER_RESOLUTION) return aggregateNewFields(ids, [ 'browserWidth', 'browserHeight' ], limit) - if (type === constants.SIZES_TYPE_SCREEN_WIDTH) return aggregateNewFields(ids, [ 'screenWidth' ], limit) - if (type === constants.SIZES_TYPE_SCREEN_HEIGHT) return aggregateNewFields(ids, [ 'screenHeight' ], limit) - if (type === constants.SIZES_TYPE_SCREEN_RESOLUTION) return aggregateNewFields(ids, [ 'screenWidth', 'screenHeight' ], limit) + if (type === constants.SIZES_TYPE_BROWSER_WIDTH) return aggregateNewRecords(ids, [ 'browserWidth' ], limit) + if (type === constants.SIZES_TYPE_BROWSER_HEIGHT) return aggregateNewRecords(ids, [ 'browserHeight' ], limit) + if (type === constants.SIZES_TYPE_BROWSER_RESOLUTION) return aggregateNewRecords(ids, [ 'browserWidth', 'browserHeight' ], limit) + if (type === constants.SIZES_TYPE_SCREEN_WIDTH) return aggregateNewRecords(ids, [ 'screenWidth' ], limit) + if (type === constants.SIZES_TYPE_SCREEN_HEIGHT) return aggregateNewRecords(ids, [ 'screenHeight' ], limit) + if (type === constants.SIZES_TYPE_SCREEN_RESOLUTION) return aggregateNewRecords(ids, [ 'screenWidth', 'screenHeight' ], limit) } if (sorting === sortings.SORTINGS_RECENT) { - if (type === constants.SIZES_TYPE_BROWSER_WIDTH) return aggregateRecentFields(ids, [ 'browserWidth' ], limit) - if (type === constants.SIZES_TYPE_BROWSER_HEIGHT) return aggregateRecentFields(ids, [ 'browserHeight' ], limit) - if (type === constants.SIZES_TYPE_BROWSER_RESOLUTION) return aggregateRecentFields(ids, [ 'browserWidth', 'browserHeight' ], limit) - if (type === constants.SIZES_TYPE_SCREEN_WIDTH) return aggregateRecentFields(ids, [ 'screenWidth' ], limit) - if (type === constants.SIZES_TYPE_SCREEN_HEIGHT) return aggregateRecentFields(ids, [ 'screenHeight' ], limit) - if (type === constants.SIZES_TYPE_SCREEN_RESOLUTION) return aggregateRecentFields(ids, [ 'screenWidth', 'screenHeight' ], limit) + if (type === constants.SIZES_TYPE_BROWSER_WIDTH) return aggregateRecentRecords(ids, [ 'browserWidth' ], limit) + if (type === constants.SIZES_TYPE_BROWSER_HEIGHT) return aggregateRecentRecords(ids, [ 'browserHeight' ], limit) + if (type === constants.SIZES_TYPE_BROWSER_RESOLUTION) return aggregateRecentRecords(ids, [ 'browserWidth', 'browserHeight' ], limit) + if (type === constants.SIZES_TYPE_SCREEN_WIDTH) return aggregateRecentRecords(ids, [ 'screenWidth' ], limit) + if (type === constants.SIZES_TYPE_SCREEN_HEIGHT) return aggregateRecentRecords(ids, [ 'screenHeight' ], limit) + if (type === constants.SIZES_TYPE_SCREEN_RESOLUTION) return aggregateRecentRecords(ids, [ 'screenWidth', 'screenHeight' ], limit) } })() diff --git a/src/database/systems.js b/src/database/systems.js index 37261a6e..e6e8ee4e 100644 --- a/src/database/systems.js +++ b/src/database/systems.js @@ -1,9 +1,9 @@ 'use strict' const Record = require('../models/Record') -const aggregateTopFields = require('../aggregations/aggregateTopFields') -const aggregateNewFields = require('../aggregations/aggregateNewFields') -const aggregateRecentFields = require('../aggregations/aggregateRecentFields') +const aggregateTopRecords = require('../aggregations/aggregateTopRecords') +const aggregateNewRecords = require('../aggregations/aggregateNewRecords') +const aggregateRecentRecords = require('../aggregations/aggregateRecentRecords') const sortings = require('../constants/sortings') const constants = require('../constants/systems') const bestMatch = require('../utils/bestMatch') @@ -26,14 +26,14 @@ const get = async (ids, sorting, type, range, limit, dateDetails) => { const aggregation = (() => { if (type === constants.SYSTEMS_TYPE_NO_VERSION) { - if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'osName' ], range, limit, dateDetails) - if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'osName' ], limit) - if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'osName' ], limit) + if (sorting === sortings.SORTINGS_TOP) return aggregateTopRecords(ids, [ 'osName' ], range, limit, dateDetails) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewRecords(ids, [ 'osName' ], limit) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentRecords(ids, [ 'osName' ], limit) } if (type === constants.SYSTEMS_TYPE_WITH_VERSION) { - if (sorting === sortings.SORTINGS_TOP) return aggregateTopFields(ids, [ 'osName', 'osVersion' ], range, limit, dateDetails) - if (sorting === sortings.SORTINGS_NEW) return aggregateNewFields(ids, [ 'osName', 'osVersion' ], limit) - if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentFields(ids, [ 'osName', 'osVersion' ], limit) + if (sorting === sortings.SORTINGS_TOP) return aggregateTopRecords(ids, [ 'osName', 'osVersion' ], range, limit, dateDetails) + if (sorting === sortings.SORTINGS_NEW) return aggregateNewRecords(ids, [ 'osName', 'osVersion' ], limit) + if (sorting === sortings.SORTINGS_RECENT) return aggregateRecentRecords(ids, [ 'osName', 'osVersion' ], limit) } })() diff --git a/test/aggregations/aggregateNewFields.js b/test/aggregations/aggregateNewFields.js index 70888db0..c8cb3bbb 100644 --- a/test/aggregations/aggregateNewFields.js +++ b/test/aggregations/aggregateNewFields.js @@ -3,11 +3,11 @@ const test = require('ava') const uuid = require('uuid').v4 -const aggregateNewFields = require('../../src/aggregations/aggregateNewFields') +const aggregateNewRecords = require('../../src/aggregations/aggregateNewRecords') test('return aggregation', async (t) => { - const result = aggregateNewFields(uuid(), [ 'siteReferrer' ]) + const result = aggregateNewRecords(uuid(), [ 'siteReferrer' ]) t.true(Array.isArray(result)) diff --git a/test/aggregations/aggregateRecentFields.js b/test/aggregations/aggregateRecentFields.js index 82887c35..fefb739f 100644 --- a/test/aggregations/aggregateRecentFields.js +++ b/test/aggregations/aggregateRecentFields.js @@ -3,11 +3,11 @@ const test = require('ava') const uuid = require('uuid').v4 -const aggregateRecentFields = require('../../src/aggregations/aggregateRecentFields') +const aggregateRecentRecords = require('../../src/aggregations/aggregateRecentRecords') test('return aggregation', async (t) => { - const result = aggregateRecentFields(uuid(), [ 'osName', 'osVersion' ]) + const result = aggregateRecentRecords(uuid(), [ 'osName', 'osVersion' ]) t.true(Array.isArray(result)) diff --git a/test/aggregations/aggregateTopFields.js b/test/aggregations/aggregateTopFields.js index 58441770..b662c755 100644 --- a/test/aggregations/aggregateTopFields.js +++ b/test/aggregations/aggregateTopFields.js @@ -3,12 +3,12 @@ const test = require('ava') const uuid = require('uuid').v4 -const aggregateTopFields = require('../../src/aggregations/aggregateTopFields') +const aggregateTopRecords = require('../../src/aggregations/aggregateTopRecords') const createDate = require('../../src/utils/createDate') test('return aggregation', async (t) => { - const result = aggregateTopFields(uuid(), [ 'osName', 'osVersion' ], createDate()) + const result = aggregateTopRecords(uuid(), [ 'osName', 'osVersion' ], createDate()) t.true(Array.isArray(result)) From 9077a5dbf7c4e6aea869c189f2ddd14a626d2784 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 15 Nov 2020 12:33:05 +0100 Subject: [PATCH 023/208] Remove redundant code --- .../components/routes/RouteSettings.js | 126 +++++------------- 1 file changed, 31 insertions(+), 95 deletions(-) diff --git a/src/ui/scripts/components/routes/RouteSettings.js b/src/ui/scripts/components/routes/RouteSettings.js index 8425710f..97505221 100644 --- a/src/ui/scripts/components/routes/RouteSettings.js +++ b/src/ui/scripts/components/routes/RouteSettings.js @@ -15,6 +15,12 @@ import LinkItem from '../LinkItem' import Line from '../Line' import Message from '../Message' +const FetchingMessage = (props) => { + + return h(Message, { status: 'warning' }, `Fetching ${ props.label }...`) + +} + const RouteSettings = (props) => { useEffect(() => { @@ -23,113 +29,43 @@ const RouteSettings = (props) => { }, []) - const showDomainAddModal = () => { - - props.addModalsModal({ - type: MODALS_DOMAIN_ADD, - props: {} - }) - - } - - const showDomainEditModal = (domain) => { - - props.addModalsModal({ - type: MODALS_DOMAIN_EDIT, - props: domain - }) - - } - - const showEventAddModal = () => { + const showModal = (type, data = {}) => { props.addModalsModal({ - type: MODALS_EVENT_ADD, - props: {} + type, + props: data }) } - const showEventEditModal = (event) => { - - props.addModalsModal({ - type: MODALS_EVENT_EDIT, - props: event - }) - - } - - const showPermanentTokenAddModal = () => { - - props.addModalsModal({ - type: MODALS_PERMANENT_TOKEN_ADD, - props: {} - }) - - } - - const showPermanentTokenEditModal = (permanentToken) => { - - props.addModalsModal({ - type: MODALS_PERMANENT_TOKEN_EDIT, - props: permanentToken - }) - - } - - const domainsFetching = [ - h(Message, { status: 'warning' }, 'Fetching domains...') - ] - - const domainsItems = [ - ...props.domains.value.map( - (domain) => [ + const createItems = (items, editFn, createFn, createLabel) => [ + ...items.map( + (item) => [ h(LinkItem, { type: 'button', - text: domain.id, - onClick: () => showDomainEditModal(domain) - }, domain.title), + text: item.id, + onClick: () => editFn(item) + }, item.title), h(Line) ] ).flat(), - h(LinkItem, { type: 'button', onClick: showDomainAddModal }, 'New domain') + h(LinkItem, { type: 'button', onClick: createFn }, createLabel) ] - const eventsFetching = [ - h(Message, { status: 'warning' }, 'Fetching events...') - ] + const showDomainAddModal = () => showModal(MODALS_DOMAIN_ADD) + const showDomainEditModal = (domain) => showModal(MODALS_DOMAIN_EDIT, domain) + const showEventAddModal = () => showModal(MODALS_EVENT_ADD) + const showEventEditModal = (event) => showModal(MODALS_EVENT_EDIT, event) + const showPermanentTokenAddModal = () => showModal(MODALS_PERMANENT_TOKEN_ADD) + const showPermanentTokenEditModal = (permanentToken) => showModal(MODALS_PERMANENT_TOKEN_EDIT, permanentToken) - const eventsItems = [ - ...props.events.value.map( - (event) => [ - h(LinkItem, { - type: 'button', - text: event.id, - onClick: () => showEventEditModal(event) - }, event.title), - h(Line) - ] - ).flat(), - h(LinkItem, { type: 'button', onClick: showEventAddModal }, 'New event') - ] - - const permanentTokensFetching = [ - h(Message, { status: 'warning' }, 'Fetching permanent tokens...') - ] + const domainsFetching = h(FetchingMessage, { label: 'domains' }) + const eventsFetching = h(FetchingMessage, { label: 'events' }) + const permanentTokensFetching = h(FetchingMessage, { label: 'permanent tokens' }) - const permanentTokensItems = [ - ...props.permanentTokens.value.map( - (permanentToken) => [ - h(LinkItem, { - type: 'button', - text: permanentToken.id, - onClick: () => showPermanentTokenEditModal(permanentToken.id, permanentToken.title) - }, permanentToken.title), - h(Line) - ] - ).flat(), - h(LinkItem, { type: 'button', onClick: showPermanentTokenAddModal }, 'New permanent token') - ] + const domainsItems = createItems(props.domains.value, showDomainEditModal, showDomainAddModal, 'New domain') + const eventsItems = createItems(props.events.value, showEventEditModal, showEventAddModal, 'New event') + const permanentTokensItems = createItems(props.domains.value, showPermanentTokenEditModal, showPermanentTokenAddModal, 'New permanent token') return ( h(Fragment, {}, @@ -145,19 +81,19 @@ const RouteSettings = (props) => { h(CardSetting, { headline: 'Domains' }, - ...(props.domains.fetching === true ? domainsFetching : domainsItems) + ...(props.domains.fetching === true ? [ domainsFetching ] : domainsItems) ), h(CardSetting, { headline: 'Events' }, - ...(props.events.fetching === true ? eventsFetching : eventsItems) + ...(props.events.fetching === true ? [ eventsFetching ] : eventsItems) ), h(CardSetting, { headline: 'Permanent Tokens' }, - ...(props.permanentTokens.fetching === true ? permanentTokensFetching : permanentTokensItems) + ...(props.permanentTokens.fetching === true ? [ permanentTokensFetching ] : permanentTokensItems) ), h(CardSetting, { From 5875c035dfa24a882479a8d9fcf07124febaae14 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 15 Nov 2020 12:42:51 +0100 Subject: [PATCH 024/208] Include fetching and error states in global fetching and errors variable --- src/ui/scripts/enhancers/enhanceState.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index 62026206..582c66e4 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -17,7 +17,9 @@ export default (state) => { state.sizes.fetching === true || state.languages.fetching === true || state.domains.fetching === true || - state.token.fetching === true + state.token.fetching === true || + state.permanentTokens.fetching === true || + state.events.fetching === true ) const errors = [ @@ -33,7 +35,9 @@ export default (state) => { state.sizes.error, state.languages.error, state.domains.error, - state.token.error + state.token.error, + state.permanentTokens.error, + state.events.error ].filter(isDefined) return Object.assign({}, state, { From b9c0f422ae21ed1c7d746fc303773df7875f67fa Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 15 Nov 2020 15:34:12 +0100 Subject: [PATCH 025/208] Load events in events and settings --- src/ui/scripts/components/Dashboard.js | 1 - src/ui/scripts/components/routes/RouteEvents.js | 8 +++++++- src/ui/scripts/components/routes/RouteSettings.js | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/components/Dashboard.js b/src/ui/scripts/components/Dashboard.js index 4bb61133..756a749c 100644 --- a/src/ui/scripts/components/Dashboard.js +++ b/src/ui/scripts/components/Dashboard.js @@ -25,7 +25,6 @@ const Dashboard = (props) => { useEffect(() => { props.fetchDomains(props) - props.fetchEvents(props) }, []) diff --git a/src/ui/scripts/components/routes/RouteEvents.js b/src/ui/scripts/components/routes/RouteEvents.js index 83d8fcc1..5c6ea579 100644 --- a/src/ui/scripts/components/routes/RouteEvents.js +++ b/src/ui/scripts/components/routes/RouteEvents.js @@ -1,4 +1,4 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h, Fragment, useEffect } from 'react' // import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' // import selectViewsValue from '../../selectors/selectViewsValue' @@ -10,6 +10,12 @@ import CardViews from '../cards/CardViews' const RouteEvents = (props) => { + useEffect(() => { + + props.fetchEvents(props) + + }, []) + return ( h(Fragment, {}, diff --git a/src/ui/scripts/components/routes/RouteSettings.js b/src/ui/scripts/components/routes/RouteSettings.js index 97505221..5b2f9f58 100644 --- a/src/ui/scripts/components/routes/RouteSettings.js +++ b/src/ui/scripts/components/routes/RouteSettings.js @@ -26,6 +26,7 @@ const RouteSettings = (props) => { useEffect(() => { props.fetchPermanentTokens(props) + props.fetchEvents(props) }, []) From 3d996b0cc3cf2ad18f2cd88d71e39c77ae0cea1c Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 15 Nov 2020 16:28:31 +0100 Subject: [PATCH 026/208] Refactor modals to include common props --- src/ui/scripts/components/Modals.js | 44 +++++++++++-------- .../components/modals/ModalDomainAdd.js | 6 +-- .../components/modals/ModalDomainEdit.js | 6 +-- .../components/modals/ModalEventAdd.js | 6 +-- .../components/modals/ModalEventEdit.js | 6 +-- .../modals/ModalPermanentTokenAdd.js | 6 +-- .../modals/ModalPermanentTokenEdit.js | 6 +-- 7 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/ui/scripts/components/Modals.js b/src/ui/scripts/components/Modals.js index 1283a4b9..a4b9a7b3 100644 --- a/src/ui/scripts/components/Modals.js +++ b/src/ui/scripts/components/Modals.js @@ -1,4 +1,5 @@ import { createElement as h, Fragment } from 'react' +import PropTypes from 'prop-types' import { MODALS_DOMAIN_ADD, @@ -17,60 +18,67 @@ import ModalEventEdit from './modals/ModalEventEdit' import ModalPermanentTokenAdd from './modals/ModalPermanentTokenAdd' import ModalPermanentTokenEdit from './modals/ModalPermanentTokenEdit' +export const commonPropTypes = { + current: PropTypes.bool.isRequired, + active: PropTypes.bool.isRequired, + closeModal: PropTypes.func.isRequired +} + const Modals = (props) => { const modals = Object.entries(props.modals.value).map(([ modalId, modalData ], index, modals) => { const current = modals.length - 1 === index + const active = modalData.visible === true const closeModal = props.removeModalsModal.bind(null, modalId) + const commonProps = { + current, + active, + closeModal + } + return ( h(Modal, { key: modalId, visible: modalData.visible }, modalData.type === MODALS_DOMAIN_ADD && h(ModalDomainAdd, { - current, + ...commonProps, fetching: props.domains.fetching, - addDomain: props.addDomain.bind(null, props), - closeModal + addDomain: props.addDomain.bind(null, props) }), modalData.type === MODALS_DOMAIN_EDIT && h(ModalDomainEdit, { - current, + ...commonProps, id: modalData.props.id, title: modalData.props.title, fetching: props.domains.fetching, updateDomain: props.updateDomain.bind(null, props), - deleteDomain: props.deleteDomain.bind(null, props), - closeModal + deleteDomain: props.deleteDomain.bind(null, props) }), modalData.type === MODALS_EVENT_ADD && h(ModalEventAdd, { - current, + ...commonProps, fetching: props.events.fetching, - addEvent: props.addEvent.bind(null, props), - closeModal + addEvent: props.addEvent.bind(null, props) }), modalData.type === MODALS_EVENT_EDIT && h(ModalEventEdit, { - current, + ...commonProps, id: modalData.props.id, title: modalData.props.title, type: modalData.props.type, fetching: props.events.fetching, updateEvent: props.updateEvent.bind(null, props), - deleteEvent: props.deleteEvent.bind(null, props), - closeModal + deleteEvent: props.deleteEvent.bind(null, props) }), modalData.type === MODALS_PERMANENT_TOKEN_ADD && h(ModalPermanentTokenAdd, { - current, + ...commonProps, fetching: props.permanentTokens.fetching, - addPermanentToken: props.addPermanentToken.bind(null, props), - closeModal + addPermanentToken: props.addPermanentToken.bind(null, props) }), modalData.type === MODALS_PERMANENT_TOKEN_EDIT && h(ModalPermanentTokenEdit, { - current, + ...commonProps, id: modalData.props.id, title: modalData.props.title, fetching: props.permanentTokens.fetching, updatePermanentToken: props.updatePermanentToken.bind(null, props), - deletePermanentToken: props.deletePermanentToken.bind(null, props), - closeModal + deletePermanentToken: props.deletePermanentToken.bind(null, props) }) ) ) diff --git a/src/ui/scripts/components/modals/ModalDomainAdd.js b/src/ui/scripts/components/modals/ModalDomainAdd.js index 1e3873eb..3cd3b0c2 100644 --- a/src/ui/scripts/components/modals/ModalDomainAdd.js +++ b/src/ui/scripts/components/modals/ModalDomainAdd.js @@ -6,6 +6,7 @@ import Input from '../Input' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' +import { commonPropTypes } from '../Modals' import shortId from '../../utils/shortId' @@ -77,10 +78,9 @@ const ModalDomainAdd = (props) => { } ModalDomainAdd.propTypes = { - current: PropTypes.bool.isRequired, + ...commonPropTypes, fetching: PropTypes.bool.isRequired, - addDomain: PropTypes.func.isRequired, - closeModal: PropTypes.func.isRequired + addDomain: PropTypes.func.isRequired } export default ModalDomainAdd \ No newline at end of file diff --git a/src/ui/scripts/components/modals/ModalDomainEdit.js b/src/ui/scripts/components/modals/ModalDomainEdit.js index 162da123..b34e4848 100644 --- a/src/ui/scripts/components/modals/ModalDomainEdit.js +++ b/src/ui/scripts/components/modals/ModalDomainEdit.js @@ -7,6 +7,7 @@ import Textarea from '../Textarea' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' +import { commonPropTypes } from '../Modals' import customTracker from '../../../../utils/customTracker' import shortId from '../../utils/shortId' @@ -128,13 +129,12 @@ const ModalDomainEdit = (props) => { } ModalDomainEdit.propTypes = { - current: PropTypes.bool.isRequired, + ...commonPropTypes, id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, fetching: PropTypes.bool.isRequired, updateDomain: PropTypes.func.isRequired, - deleteDomain: PropTypes.func.isRequired, - closeModal: PropTypes.func.isRequired + deleteDomain: PropTypes.func.isRequired } export default ModalDomainEdit \ No newline at end of file diff --git a/src/ui/scripts/components/modals/ModalEventAdd.js b/src/ui/scripts/components/modals/ModalEventAdd.js index 6f278a5d..80b4ca7b 100644 --- a/src/ui/scripts/components/modals/ModalEventAdd.js +++ b/src/ui/scripts/components/modals/ModalEventAdd.js @@ -9,6 +9,7 @@ import Select from '../Select' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' +import { commonPropTypes } from '../Modals' import shortId from '../../utils/shortId' @@ -102,10 +103,9 @@ const ModalEventAdd = (props) => { } ModalEventAdd.propTypes = { - current: PropTypes.bool.isRequired, + ...commonPropTypes, fetching: PropTypes.bool.isRequired, - addEvent: PropTypes.func.isRequired, - closeModal: PropTypes.func.isRequired + addEvent: PropTypes.func.isRequired } export default ModalEventAdd \ No newline at end of file diff --git a/src/ui/scripts/components/modals/ModalEventEdit.js b/src/ui/scripts/components/modals/ModalEventEdit.js index 29c7f284..9f70dd8f 100644 --- a/src/ui/scripts/components/modals/ModalEventEdit.js +++ b/src/ui/scripts/components/modals/ModalEventEdit.js @@ -10,6 +10,7 @@ import Textarea from '../Textarea' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' +import { commonPropTypes } from '../Modals' import shortId from '../../utils/shortId' @@ -148,13 +149,12 @@ const ModalEventEdit = (props) => { } ModalEventEdit.propTypes = { - current: PropTypes.bool.isRequired, + ...commonPropTypes, id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, fetching: PropTypes.bool.isRequired, updateEvent: PropTypes.func.isRequired, - deleteEvent: PropTypes.func.isRequired, - closeModal: PropTypes.func.isRequired + deleteEvent: PropTypes.func.isRequired } export default ModalEventEdit \ No newline at end of file diff --git a/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js b/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js index 147d8808..bcf04f6f 100644 --- a/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js +++ b/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js @@ -6,6 +6,7 @@ import Input from '../Input' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' +import { commonPropTypes } from '../Modals' import shortId from '../../utils/shortId' @@ -77,10 +78,9 @@ const ModalPermanentTokenAdd = (props) => { } ModalPermanentTokenAdd.propTypes = { - current: PropTypes.bool.isRequired, + ...commonPropTypes, fetching: PropTypes.bool.isRequired, - addPermanentToken: PropTypes.func.isRequired, - closeModal: PropTypes.func.isRequired + addPermanentToken: PropTypes.func.isRequired } export default ModalPermanentTokenAdd \ No newline at end of file diff --git a/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js b/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js index 31b0eb75..852ec018 100644 --- a/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js +++ b/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js @@ -6,6 +6,7 @@ import Input from '../Input' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' +import { commonPropTypes } from '../Modals' import shortId from '../../utils/shortId' @@ -110,13 +111,12 @@ const ModalPermanentTokenEdit = (props) => { } ModalPermanentTokenEdit.propTypes = { - current: PropTypes.bool.isRequired, + ...commonPropTypes, id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, fetching: PropTypes.bool.isRequired, updatePermanentToken: PropTypes.func.isRequired, - deletePermanentToken: PropTypes.func.isRequired, - closeModal: PropTypes.func.isRequired + deletePermanentToken: PropTypes.func.isRequired } export default ModalPermanentTokenEdit \ No newline at end of file From 284b707bbec0d6a5340e3af851531abbc8b32a27 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 15 Nov 2020 16:34:14 +0100 Subject: [PATCH 027/208] Avoid that it's possible to trigger button in closing modals --- src/ui/scripts/components/modals/ModalDomainAdd.js | 5 +++-- src/ui/scripts/components/modals/ModalDomainEdit.js | 8 +++++--- src/ui/scripts/components/modals/ModalEventAdd.js | 5 +++-- src/ui/scripts/components/modals/ModalEventEdit.js | 8 +++++--- .../scripts/components/modals/ModalPermanentTokenAdd.js | 5 +++-- .../scripts/components/modals/ModalPermanentTokenEdit.js | 8 +++++--- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/ui/scripts/components/modals/ModalDomainAdd.js b/src/ui/scripts/components/modals/ModalDomainAdd.js index 3cd3b0c2..cc69589b 100644 --- a/src/ui/scripts/components/modals/ModalDomainAdd.js +++ b/src/ui/scripts/components/modals/ModalDomainAdd.js @@ -59,7 +59,8 @@ const ModalDomainAdd = (props) => { h('button', { type: 'button', className: 'card__button link', - onClick: props.closeModal + onClick: props.closeModal, + disabled: props.active === false }, 'Close'), h('div', { @@ -68,7 +69,7 @@ const ModalDomainAdd = (props) => { h('button', { className: 'card__button card__button--primary link color-white', - disabled: props.fetching === true + disabled: props.fetching === true || props.active === false }, props.fetching === true ? h(Spinner) : 'Add') ) diff --git a/src/ui/scripts/components/modals/ModalDomainEdit.js b/src/ui/scripts/components/modals/ModalDomainEdit.js index b34e4848..d223c254 100644 --- a/src/ui/scripts/components/modals/ModalDomainEdit.js +++ b/src/ui/scripts/components/modals/ModalDomainEdit.js @@ -100,7 +100,8 @@ const ModalDomainEdit = (props) => { h('button', { type: 'button', className: 'card__button link', - onClick: props.closeModal + onClick: props.closeModal, + disabled: props.active === false }, 'Close'), h('div', { @@ -110,7 +111,8 @@ const ModalDomainEdit = (props) => { h('button', { type: 'button', className: 'card__button link color-destructive', - onClick: deleteDomain + onClick: deleteDomain, + disabled: props.active === false }, 'Delete'), h('div', { @@ -119,7 +121,7 @@ const ModalDomainEdit = (props) => { h('button', { className: 'card__button card__button--primary link color-white', - disabled: props.fetching === true + disabled: props.fetching === true || props.active === false }, props.fetching === true ? h(Spinner) : 'Rename') ) diff --git a/src/ui/scripts/components/modals/ModalEventAdd.js b/src/ui/scripts/components/modals/ModalEventAdd.js index 80b4ca7b..fcc44acf 100644 --- a/src/ui/scripts/components/modals/ModalEventAdd.js +++ b/src/ui/scripts/components/modals/ModalEventAdd.js @@ -84,7 +84,8 @@ const ModalEventAdd = (props) => { h('button', { type: 'button', className: 'card__button link', - onClick: props.closeModal + onClick: props.closeModal, + disabled: props.active === false }, 'Close'), h('div', { @@ -93,7 +94,7 @@ const ModalEventAdd = (props) => { h('button', { className: 'card__button card__button--primary link color-white', - disabled: props.fetching === true + disabled: props.fetching === true || props.active === false }, props.fetching === true ? h(Spinner) : 'Add') ) diff --git a/src/ui/scripts/components/modals/ModalEventEdit.js b/src/ui/scripts/components/modals/ModalEventEdit.js index 9f70dd8f..85a234fd 100644 --- a/src/ui/scripts/components/modals/ModalEventEdit.js +++ b/src/ui/scripts/components/modals/ModalEventEdit.js @@ -120,7 +120,8 @@ const ModalEventEdit = (props) => { h('button', { type: 'button', className: 'card__button link', - onClick: props.closeModal + onClick: props.closeModal, + disabled: props.active === false }, 'Close'), h('div', { @@ -130,7 +131,8 @@ const ModalEventEdit = (props) => { h('button', { type: 'button', className: 'card__button link color-destructive', - onClick: deleteEvent + onClick: deleteEvent, + disabled: props.active === false }, 'Delete'), h('div', { @@ -139,7 +141,7 @@ const ModalEventEdit = (props) => { h('button', { className: 'card__button card__button--primary link color-white', - disabled: props.fetching === true + disabled: props.fetching === true || props.active === false }, props.fetching === true ? h(Spinner) : 'Edit') ) diff --git a/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js b/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js index bcf04f6f..b77a236d 100644 --- a/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js +++ b/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js @@ -59,7 +59,8 @@ const ModalPermanentTokenAdd = (props) => { h('button', { type: 'button', className: 'card__button link', - onClick: props.closeModal + onClick: props.closeModal, + disabled: props.active === false }, 'Close'), h('div', { @@ -68,7 +69,7 @@ const ModalPermanentTokenAdd = (props) => { h('button', { className: 'card__button card__button--primary link color-white', - disabled: props.fetching === true + disabled: props.fetching === true || props.active === false }, props.fetching === true ? h(Spinner) : 'Add') ) diff --git a/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js b/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js index 852ec018..669b57fd 100644 --- a/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js +++ b/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js @@ -82,7 +82,8 @@ const ModalPermanentTokenEdit = (props) => { h('button', { type: 'button', className: 'card__button link', - onClick: props.closeModal + onClick: props.closeModal, + disabled: props.active === false }, 'Close'), h('div', { @@ -92,7 +93,8 @@ const ModalPermanentTokenEdit = (props) => { h('button', { type: 'button', className: 'card__button link color-destructive', - onClick: deletePermanentToken + onClick: deletePermanentToken, + disabled: props.active === false }, 'Delete'), h('div', { @@ -101,7 +103,7 @@ const ModalPermanentTokenEdit = (props) => { h('button', { className: 'card__button card__button--primary link color-white', - disabled: props.fetching === true + disabled: props.fetching === true || props.active === false }, props.fetching === true ? h(Spinner) : 'Rename') ) From 1c9c322b0f91f89050200285ce85cb8b36cafd90 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 15 Nov 2020 16:35:24 +0100 Subject: [PATCH 028/208] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 122a2e93..387b9118 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Close, delete and submit in modals could be triggered multiple times + ## [2.4.0] - 2020-11-15 Ackee now ignores your own visits once you have logged into the dashboard. Make sure to enable the [`ignoreOwnVisits` option in ackee-tracker](https://github.com/electerious/ackee-tracker#-options) to use this feature. It's currently opt-in, because it requires [a new `Access-Control-Allow-Credentials` header](docs/CORS%20headers.md#credentials), which wasn't previously required. It will be turned on by default in the next major release of Ackee. From c3f45595baaf6f436262439668b124dd385f409f Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 13:12:08 +0100 Subject: [PATCH 029/208] New widget loader --- src/ui/scripts/actions/index.js | 3 +- src/ui/scripts/actions/languages.js | 68 ------------------- src/ui/scripts/actions/pages.js | 68 ------------------- src/ui/scripts/actions/widgets.js | 62 +++++++++++++++++ .../components/routes/RouteLanguages.js | 40 +++++++---- .../scripts/components/routes/RoutePages.js | 40 +++++++---- src/ui/scripts/enhancers/enhanceState.js | 4 -- src/ui/scripts/loaders/languagesLoader.js | 36 ++++++++++ src/ui/scripts/loaders/pagesLoader.js | 36 ++++++++++ src/ui/scripts/reducers/index.js | 6 +- src/ui/scripts/reducers/languages.js | 35 ---------- src/ui/scripts/reducers/pages.js | 35 ---------- src/ui/scripts/reducers/widgets.js | 43 ++++++++++++ .../scripts/selectors/selectLanguagesValue.js | 6 -- src/ui/scripts/selectors/selectPagesValue.js | 6 -- 15 files changed, 236 insertions(+), 252 deletions(-) delete mode 100644 src/ui/scripts/actions/languages.js delete mode 100644 src/ui/scripts/actions/pages.js create mode 100644 src/ui/scripts/actions/widgets.js create mode 100644 src/ui/scripts/loaders/languagesLoader.js create mode 100644 src/ui/scripts/loaders/pagesLoader.js delete mode 100644 src/ui/scripts/reducers/languages.js delete mode 100644 src/ui/scripts/reducers/pages.js create mode 100644 src/ui/scripts/reducers/widgets.js delete mode 100644 src/ui/scripts/selectors/selectLanguagesValue.js delete mode 100644 src/ui/scripts/selectors/selectPagesValue.js diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index 22c136f4..91e5c032 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -7,14 +7,13 @@ export * from './domains' export * from './events' export * from './overview' export * from './views' -export * from './pages' export * from './referrers' export * from './durations' -export * from './languages' export * from './sizes' export * from './systems' export * from './devices' export * from './browsers' +export * from './widgets' export const RESET_STATE = Symbol() diff --git a/src/ui/scripts/actions/languages.js b/src/ui/scripts/actions/languages.js deleted file mode 100644 index 90ccdb27..00000000 --- a/src/ui/scripts/actions/languages.js +++ /dev/null @@ -1,68 +0,0 @@ -import api from '../utils/api' -import signalHandler from '../utils/signalHandler' - -export const SET_LANGUAGES_VALUE = Symbol() -export const SET_LANGUAGES_FETCHING = Symbol() -export const SET_LANGUAGES_ERROR = Symbol() - -export const setLanguagesValue = (domainId, payload) => ({ - type: SET_LANGUAGES_VALUE, - domainId, - payload -}) - -export const setLanguagesFetching = (payload) => ({ - type: SET_LANGUAGES_FETCHING, - payload -}) - -export const setLanguagesError = (payload) => ({ - type: SET_LANGUAGES_ERROR, - payload -}) - -export const fetchLanguages = signalHandler((signal) => (props) => async (dispatch) => { - - dispatch(setLanguagesFetching(true)) - dispatch(setLanguagesError()) - - try { - - const data = await api({ - query: ` - query fetchLanguages($sorting: Sorting!, $range: Range) { - domains { - id - statistics { - languages(sorting: $sorting, range: $range) { - id - count - created - } - } - } - } - `, - variables: { - sorting: props.filter.sorting, - range: props.filter.range - }, - props, - signal: signal() - }) - - data.domains.forEach((domain) => { - dispatch(setLanguagesValue(domain.id, domain.statistics.languages)) - }) - dispatch(setLanguagesFetching(false)) - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setLanguagesFetching(false)) - if (err.name === 'HandledError') return - dispatch(setLanguagesError(err)) - - } - -}) \ No newline at end of file diff --git a/src/ui/scripts/actions/pages.js b/src/ui/scripts/actions/pages.js deleted file mode 100644 index b47720c5..00000000 --- a/src/ui/scripts/actions/pages.js +++ /dev/null @@ -1,68 +0,0 @@ -import api from '../utils/api' -import signalHandler from '../utils/signalHandler' - -export const SET_PAGES_VALUE = Symbol() -export const SET_PAGES_FETCHING = Symbol() -export const SET_PAGES_ERROR = Symbol() - -export const setPagesValue = (domainId, payload) => ({ - type: SET_PAGES_VALUE, - domainId, - payload -}) - -export const setPagesFetching = (payload) => ({ - type: SET_PAGES_FETCHING, - payload -}) - -export const setPagesError = (payload) => ({ - type: SET_PAGES_ERROR, - payload -}) - -export const fetchPages = signalHandler((signal) => (props) => async (dispatch) => { - - dispatch(setPagesFetching(true)) - dispatch(setPagesError()) - - try { - - const data = await api({ - query: ` - query fetchPages($sorting: Sorting!, $range: Range) { - domains { - id - statistics { - pages(sorting: $sorting, range: $range) { - id - count - created - } - } - } - } - `, - variables: { - sorting: props.filter.sorting, - range: props.filter.range - }, - props, - signal: signal() - }) - - data.domains.forEach((domain) => { - dispatch(setPagesValue(domain.id, domain.statistics.pages)) - }) - dispatch(setPagesFetching(false)) - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setPagesFetching(false)) - if (err.name === 'HandledError') return - dispatch(setPagesError(err)) - - } - -}) \ No newline at end of file diff --git a/src/ui/scripts/actions/widgets.js b/src/ui/scripts/actions/widgets.js new file mode 100644 index 00000000..fc5f13fd --- /dev/null +++ b/src/ui/scripts/actions/widgets.js @@ -0,0 +1,62 @@ +import api from '../utils/api' +import signalHandler from '../utils/signalHandler' + +export const SET_WIDGETS_VALUE = Symbol() +export const SET_WIDGETS_VARIABLES = Symbol() +export const SET_WIDGETS_FETCHING = Symbol() +export const SET_WIDGETS_ERROR = Symbol() + +export const setWidgetsValue = (id, payload) => ({ + type: SET_WIDGETS_VALUE, + id, + payload +}) + +export const setWidgetsVariables = (id, payload) => ({ + type: SET_WIDGETS_VARIABLES, + id, + payload +}) + +export const setWidgetsFetching = (id, payload) => ({ + type: SET_WIDGETS_FETCHING, + id, + payload +}) + +export const setWidgetsError = (id, payload) => ({ + type: SET_WIDGETS_ERROR, + id, + payload +}) + +export const fetchWidget = signalHandler((signal) => (props, loader) => async (dispatch) => { + + const { id, query, variables, selector } = loader + + dispatch(setWidgetsVariables(id, variables)) + dispatch(setWidgetsFetching(id, true)) + dispatch(setWidgetsError(id)) + + try { + + const data = await api({ + query, + variables, + props, + signal: signal(id) + }) + + dispatch(setWidgetsValue(id, selector(data))) + dispatch(setWidgetsFetching(id, false)) + + } catch (err) { + + if (err.name === 'AbortError') return + dispatch(setWidgetsFetching(id, false)) + if (err.name === 'HandledError') return + dispatch(setWidgetsError(id, err)) + + } + +}) \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index aceda7a7..bff146ed 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -1,6 +1,7 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h, Fragment, useMemo } from 'react' -import selectLanguagesValue from '../../selectors/selectLanguagesValue' +// import selectLanguagesValue from '../../selectors/selectLanguagesValue' +import languagesLoader from '../../loaders/languagesLoader' import enhanceLanguages from '../../enhancers/enhanceLanguages' import overviewRoute from '../../utils/overviewRoute' @@ -8,27 +9,42 @@ import CardLanguages from '../cards/CardLanguages' const RouteLanguages = (props) => { - useEffect(() => { + const widgetIds = useMemo(() => { - props.fetchLanguages(props) + return props.domains.value.map( + (domain) => { + const loader = languagesLoader(domain.id, { + range: props.filter.range, + sorting: props.filter.sorting + }) - }, [ props.filter.range, props.filter.sorting ]) + props.fetchWidget(props, loader) + return loader.id + } + ) + + }, [ props.domains.value, props.filter.range, props.filter.sorting ]) return ( h(Fragment, {}, props.domains.value.map( - (domain) => ( - h(CardLanguages, { + (domain, index) => { + const widgetId = widgetIds[index] + const widget = props.widgets.value[widgetId] + + if (widget == null) return h('p', { key: domain.id }, 'empty') + + return h(CardLanguages, { key: domain.id, headline: domain.title, - range: props.filter.range, - sorting: props.filter.sorting, - loading: props.languages.fetching, - items: enhanceLanguages(selectLanguagesValue(props, domain.id).value), + range: widget.variables.range, + sorting: widget.variables.sorting, + loading: widget.fetching, + items: enhanceLanguages(widget.value), onMore: () => props.setRoute(overviewRoute(domain)) }) - ) + } ) ) diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index 29263683..da1bd6e7 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -1,34 +1,50 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h, Fragment, useMemo } from 'react' -import selectPagesValue from '../../selectors/selectPagesValue' +// import selectPagesValue from '../../selectors/selectPagesValue' import enhancePages from '../../enhancers/enhancePages' +import pagesLoader from '../../loaders/pagesLoader' import overviewRoute from '../../utils/overviewRoute' import CardPages from '../cards/CardPages' const RoutePages = (props) => { - useEffect(() => { + const widgetIds = useMemo(() => { - props.fetchPages(props) + return props.domains.value.map( + (domain) => { + const loader = pagesLoader(domain.id, { + range: props.filter.range, + sorting: props.filter.sorting + }) - }, [ props.filter.range, props.filter.sorting ]) + props.fetchWidget(props, loader) + return loader.id + } + ) + + }, [ props.domains.value, props.filter.range, props.filter.sorting ]) return ( h(Fragment, {}, props.domains.value.map( - (domain) => ( - h(CardPages, { + (domain, index) => { + const widgetId = widgetIds[index] + const widget = props.widgets.value[widgetId] + + if (widget == null) return h('p', { key: domain.id }, 'empty') + + return h(CardPages, { key: domain.id, headline: domain.title, - range: props.filter.range, - sorting: props.filter.sorting, - loading: props.pages.fetching, - items: enhancePages(selectPagesValue(props, domain.id).value), + range: widget.variables.range, + sorting: widget.variables.sorting, + loading: widget.fetching, + items: enhancePages(widget.value), onMore: () => props.setRoute(overviewRoute(domain)) }) - ) + } ) ) diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index 582c66e4..a1bc9699 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -8,14 +8,12 @@ export default (state) => { selectOverviewValue.withoutType(state, ALL_DOMAINS).fetching === true || Object.values(state.overview.value).some((value) => value.fetching) === true || state.views.fetching === true || - state.pages.fetching === true || state.referrers.fetching === true || state.durations.fetching === true || state.systems.fetching === true || state.devices.fetching === true || state.browsers.fetching === true || state.sizes.fetching === true || - state.languages.fetching === true || state.domains.fetching === true || state.token.fetching === true || state.permanentTokens.fetching === true || @@ -26,14 +24,12 @@ export default (state) => { selectOverviewValue.withoutType(state, ALL_DOMAINS).error, ...Object.values(state.overview.value).map((value) => value.error), state.views.error, - state.pages.error, state.referrers.error, state.durations.error, state.systems.error, state.devices.error, state.browsers.error, state.sizes.error, - state.languages.error, state.domains.error, state.token.error, state.permanentTokens.error, diff --git a/src/ui/scripts/loaders/languagesLoader.js b/src/ui/scripts/loaders/languagesLoader.js new file mode 100644 index 00000000..a34b8422 --- /dev/null +++ b/src/ui/scripts/loaders/languagesLoader.js @@ -0,0 +1,36 @@ +export default (domainId, opts) => { + + // TODO: Improve ids + const id = `${ domainId }${ JSON.stringify(opts) }` + + const query = ` + query fetchLanguages($domainId: ID!, $sorting: Sorting!, $range: Range) { + domain(id: $domainId) { + id + statistics { + languages(sorting: $sorting, range: $range) { + id + count + created + } + } + } + } + ` + + const variables = { + domainId, + sorting: opts.sorting, + range: opts.range + } + + const selector = (data) => data.domain.statistics.languages + + return { + id, + query, + variables, + selector + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/pagesLoader.js b/src/ui/scripts/loaders/pagesLoader.js new file mode 100644 index 00000000..6ea54ccb --- /dev/null +++ b/src/ui/scripts/loaders/pagesLoader.js @@ -0,0 +1,36 @@ +export default (domainId, opts) => { + + // TODO: Improve ids + const id = `${ domainId }${ JSON.stringify(opts) }` + + const query = ` + query fetchPages($domainId: ID!, $sorting: Sorting!, $range: Range) { + domain(id: $domainId) { + id + statistics { + pages(sorting: $sorting, range: $range) { + id + count + created + } + } + } + } + ` + + const variables = { + domainId, + sorting: opts.sorting, + range: opts.range + } + + const selector = (data) => data.domain.statistics.pages + + return { + id, + query, + variables, + selector + } + +} \ No newline at end of file diff --git a/src/ui/scripts/reducers/index.js b/src/ui/scripts/reducers/index.js index 57d71352..e0807f93 100644 --- a/src/ui/scripts/reducers/index.js +++ b/src/ui/scripts/reducers/index.js @@ -13,14 +13,13 @@ import domains from './domains' import events from './events' import overview from './overview' import views from './views' -import pages from './pages' import referrers from './referrers' import durations from './durations' import systems from './systems' import devices from './devices' import browsers from './browsers' import sizes from './sizes' -import languages from './languages' +import widgets from './widgets' const reducers = combineReducers({ modals, @@ -32,14 +31,13 @@ const reducers = combineReducers({ events, overview, views, - pages, referrers, durations, systems, devices, browsers, sizes, - languages + widgets }) export default (state, action) => { diff --git a/src/ui/scripts/reducers/languages.js b/src/ui/scripts/reducers/languages.js deleted file mode 100644 index 3588a19d..00000000 --- a/src/ui/scripts/reducers/languages.js +++ /dev/null @@ -1,35 +0,0 @@ -import produce from 'immer' - -import { - SET_LANGUAGES_VALUE, - SET_LANGUAGES_FETCHING, - SET_LANGUAGES_ERROR -} from '../actions' - -import genericState from '../utils/genericState' -import genericSubState from '../utils/genericSubState' - -export const initialState = genericState - -export const initialSubState = genericSubState - -export default produce((draft, action) => { - - const hasDomainId = () => action.domainId != null - const hasDomainValue = () => draft.value[action.domainId] != null - - if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() - - switch (action.type) { - case SET_LANGUAGES_VALUE: - draft.value[action.domainId].value = action.payload || initialSubState().value - break - case SET_LANGUAGES_FETCHING: - draft.fetching = action.payload || initialState().fetching - break - case SET_LANGUAGES_ERROR: - draft.error = action.payload || initialState().error - break - } - -}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/pages.js b/src/ui/scripts/reducers/pages.js deleted file mode 100644 index e97c75f7..00000000 --- a/src/ui/scripts/reducers/pages.js +++ /dev/null @@ -1,35 +0,0 @@ -import produce from 'immer' - -import { - SET_PAGES_VALUE, - SET_PAGES_FETCHING, - SET_PAGES_ERROR -} from '../actions' - -import genericState from '../utils/genericState' -import genericSubState from '../utils/genericSubState' - -export const initialState = genericState - -export const initialSubState = genericSubState - -export default produce((draft, action) => { - - const hasDomainId = () => action.domainId != null - const hasDomainValue = () => draft.value[action.domainId] != null - - if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() - - switch (action.type) { - case SET_PAGES_VALUE: - draft.value[action.domainId].value = action.payload || initialSubState().value - break - case SET_PAGES_FETCHING: - draft.fetching = action.payload || initialState().fetching - break - case SET_PAGES_ERROR: - draft.error = action.payload || initialState().error - break - } - -}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/widgets.js b/src/ui/scripts/reducers/widgets.js new file mode 100644 index 00000000..7d15eb71 --- /dev/null +++ b/src/ui/scripts/reducers/widgets.js @@ -0,0 +1,43 @@ +import produce from 'immer' + +import { + SET_WIDGETS_VALUE, + SET_WIDGETS_VARIABLES, + SET_WIDGETS_FETCHING, + SET_WIDGETS_ERROR +} from '../actions' + +export const initialState = () => ({ + value: {} +}) + +export const initialSubState = () => ({ + value: [], + variables: {}, + fetching: false, + error: undefined +}) + +export default produce((draft, action) => { + + const hasId = () => action.id != null + const hasValue = () => draft.value[action.id] != null + + if (hasId() === true && hasValue() === false) draft.value[action.id] = initialSubState() + + switch (action.type) { + case SET_WIDGETS_VALUE: + draft.value[action.id].value = action.payload || initialSubState().value + break + case SET_WIDGETS_VARIABLES: + draft.value[action.id].variables = action.payload || initialSubState().value + break + case SET_WIDGETS_FETCHING: + draft.value[action.id].fetching = action.payload || initialSubState().fetching + break + case SET_WIDGETS_ERROR: + draft.value[action.id].error = action.payload || initialSubState().error + break + } + +}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/selectors/selectLanguagesValue.js b/src/ui/scripts/selectors/selectLanguagesValue.js deleted file mode 100644 index 1f584def..00000000 --- a/src/ui/scripts/selectors/selectLanguagesValue.js +++ /dev/null @@ -1,6 +0,0 @@ -import { initialSubState } from '../reducers/languages' - -export default (state, domainId) => { - const value = state.languages.value[domainId] - return value == null ? initialSubState() : value -} \ No newline at end of file diff --git a/src/ui/scripts/selectors/selectPagesValue.js b/src/ui/scripts/selectors/selectPagesValue.js deleted file mode 100644 index d797f828..00000000 --- a/src/ui/scripts/selectors/selectPagesValue.js +++ /dev/null @@ -1,6 +0,0 @@ -import { initialSubState } from '../reducers/pages' - -export default (state, domainId) => { - const value = state.pages.value[domainId] - return value == null ? initialSubState() : value -} \ No newline at end of file From 2b29dbddf229bea83b68173ed92e6f77ffc45443 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 13:12:22 +0100 Subject: [PATCH 030/208] Remove unused import --- src/ui/scripts/components/routes/RouteLanguages.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index bff146ed..4357dca4 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -1,6 +1,5 @@ import { createElement as h, Fragment, useMemo } from 'react' -// import selectLanguagesValue from '../../selectors/selectLanguagesValue' import languagesLoader from '../../loaders/languagesLoader' import enhanceLanguages from '../../enhancers/enhanceLanguages' import overviewRoute from '../../utils/overviewRoute' From 87c647bd9aaca051ffdb81439790fde535d7e1e1 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 13:41:04 +0100 Subject: [PATCH 031/208] Improve ids --- src/ui/scripts/loaders/languagesLoader.js | 2 +- src/ui/scripts/loaders/pagesLoader.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/loaders/languagesLoader.js b/src/ui/scripts/loaders/languagesLoader.js index a34b8422..6af75723 100644 --- a/src/ui/scripts/loaders/languagesLoader.js +++ b/src/ui/scripts/loaders/languagesLoader.js @@ -1,7 +1,7 @@ export default (domainId, opts) => { // TODO: Improve ids - const id = `${ domainId }${ JSON.stringify(opts) }` + const id = `fetchLanguages${ domainId }${ JSON.stringify(opts) }` const query = ` query fetchLanguages($domainId: ID!, $sorting: Sorting!, $range: Range) { diff --git a/src/ui/scripts/loaders/pagesLoader.js b/src/ui/scripts/loaders/pagesLoader.js index 6ea54ccb..2011abfa 100644 --- a/src/ui/scripts/loaders/pagesLoader.js +++ b/src/ui/scripts/loaders/pagesLoader.js @@ -1,7 +1,7 @@ export default (domainId, opts) => { // TODO: Improve ids - const id = `${ domainId }${ JSON.stringify(opts) }` + const id = `fetchPages${ domainId }${ JSON.stringify(opts) }` const query = ` query fetchPages($domainId: ID!, $sorting: Sorting!, $range: Range) { From 1c0f6ec7c0ac8298e3ceeeba3b8d301af485b386 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 14:21:16 +0100 Subject: [PATCH 032/208] Avoid state update in useMemo --- .../components/routes/RouteLanguages.js | 28 +++++++++++-------- .../scripts/components/routes/RoutePages.js | 28 +++++++++++-------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index 4357dca4..a2d9e10b 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -1,4 +1,4 @@ -import { createElement as h, Fragment, useMemo } from 'react' +import { createElement as h, Fragment, useEffect, useCallback } from 'react' import languagesLoader from '../../loaders/languagesLoader' import enhanceLanguages from '../../enhancers/enhanceLanguages' @@ -8,29 +8,33 @@ import CardLanguages from '../cards/CardLanguages' const RouteLanguages = (props) => { - const widgetIds = useMemo(() => { + const createLoader = useCallback((domainId) => { + + return languagesLoader(domainId, { + range: props.filter.range, + sorting: props.filter.sorting + }) + + }, [ props.filter.range, props.filter.sorting ]) + + useEffect(() => { return props.domains.value.map( (domain) => { - const loader = languagesLoader(domain.id, { - range: props.filter.range, - sorting: props.filter.sorting - }) - + const loader = createLoader(domain.id) props.fetchWidget(props, loader) - return loader.id } ) - }, [ props.domains.value, props.filter.range, props.filter.sorting ]) + }, [ props.domains.value, createLoader ]) return ( h(Fragment, {}, props.domains.value.map( - (domain, index) => { - const widgetId = widgetIds[index] - const widget = props.widgets.value[widgetId] + (domain) => { + const { id } = createLoader(domain.id) + const widget = props.widgets.value[id] if (widget == null) return h('p', { key: domain.id }, 'empty') diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index da1bd6e7..39f3fbec 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -1,4 +1,4 @@ -import { createElement as h, Fragment, useMemo } from 'react' +import { createElement as h, Fragment, useEffect, useCallback } from 'react' // import selectPagesValue from '../../selectors/selectPagesValue' import enhancePages from '../../enhancers/enhancePages' @@ -9,29 +9,33 @@ import CardPages from '../cards/CardPages' const RoutePages = (props) => { - const widgetIds = useMemo(() => { + const createLoader = useCallback((domainId) => { + + return pagesLoader(domainId, { + range: props.filter.range, + sorting: props.filter.sorting + }) + + }, [ props.filter.range, props.filter.sorting ]) + + useEffect(() => { return props.domains.value.map( (domain) => { - const loader = pagesLoader(domain.id, { - range: props.filter.range, - sorting: props.filter.sorting - }) - + const loader = createLoader(domain.id) props.fetchWidget(props, loader) - return loader.id } ) - }, [ props.domains.value, props.filter.range, props.filter.sorting ]) + }, [ props.domains.value, createLoader ]) return ( h(Fragment, {}, props.domains.value.map( - (domain, index) => { - const widgetId = widgetIds[index] - const widget = props.widgets.value[widgetId] + (domain) => { + const { id } = createLoader(domain.id) + const widget = props.widgets.value[id] if (widget == null) return h('p', { key: domain.id }, 'empty') From 21d81ae0ad2162bf042fa3af50437fb6c290228f Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 16:37:36 +0100 Subject: [PATCH 033/208] Refactor views with widgets --- src/ui/scripts/actions/filter.js | 6 ++ src/ui/scripts/actions/index.js | 1 - src/ui/scripts/actions/views.js | 73 ------------------- src/ui/scripts/components/Filter.js | 6 +- .../scripts/components/routes/RouteViews.js | 63 ++++++++++------ src/ui/scripts/enhancers/enhanceState.js | 2 - src/ui/scripts/index.js | 8 +- src/ui/scripts/loaders/viewsLoader.js | 35 +++++++++ src/ui/scripts/reducers/filter.js | 10 ++- src/ui/scripts/reducers/index.js | 2 - src/ui/scripts/reducers/views.js | 43 ----------- src/ui/scripts/selectors/selectViewsValue.js | 11 +-- 12 files changed, 101 insertions(+), 159 deletions(-) delete mode 100644 src/ui/scripts/actions/views.js create mode 100644 src/ui/scripts/loaders/viewsLoader.js delete mode 100644 src/ui/scripts/reducers/views.js diff --git a/src/ui/scripts/actions/filter.js b/src/ui/scripts/actions/filter.js index b0dbc4fd..5a00f4a7 100644 --- a/src/ui/scripts/actions/filter.js +++ b/src/ui/scripts/actions/filter.js @@ -1,6 +1,7 @@ export const SET_FILTER_SORTING = Symbol() export const SET_FILTER_RANGE = Symbol() export const SET_FILTER_INTERVAL = Symbol() +export const SET_FILTER_VIEWS_TYPE = Symbol() export const setFilterSorting = (payload) => ({ type: SET_FILTER_SORTING, @@ -15,4 +16,9 @@ export const setFilterRange = (payload) => ({ export const setFilterInterval = (payload) => ({ type: SET_FILTER_INTERVAL, payload +}) + +export const setFilterViewsType = (payload) => ({ + type: SET_FILTER_VIEWS_TYPE, + payload }) \ No newline at end of file diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index 91e5c032..d50ed09d 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -6,7 +6,6 @@ export * from './filter' export * from './domains' export * from './events' export * from './overview' -export * from './views' export * from './referrers' export * from './durations' export * from './sizes' diff --git a/src/ui/scripts/actions/views.js b/src/ui/scripts/actions/views.js deleted file mode 100644 index 9b8a4a82..00000000 --- a/src/ui/scripts/actions/views.js +++ /dev/null @@ -1,73 +0,0 @@ -import api from '../utils/api' -import signalHandler from '../utils/signalHandler' - -export const SET_VIEWS_TYPE = Symbol() -export const SET_VIEWS_VALUE = Symbol() -export const SET_VIEWS_FETCHING = Symbol() -export const SET_VIEWS_ERROR = Symbol() - -export const setViewsType = (payload) => ({ - type: SET_VIEWS_TYPE, - payload -}) - -export const setViewsValue = (domainId, payload) => ({ - type: SET_VIEWS_VALUE, - domainId, - payload -}) - -export const setViewsFetching = (payload) => ({ - type: SET_VIEWS_FETCHING, - payload -}) - -export const setViewsError = (payload) => ({ - type: SET_VIEWS_ERROR, - payload -}) - -export const fetchViews = signalHandler((signal) => (props) => async (dispatch) => { - - dispatch(setViewsFetching(true)) - dispatch(setViewsError()) - - try { - - const data = await api({ - query: ` - query fetchViews($interval: Interval!, $type: ViewType!) { - domains { - id - statistics { - views(interval: $interval, type: $type) { - id - count - } - } - } - } - `, - variables: { - interval: props.filter.interval, - type: props.views.type - }, - props, - signal: signal() - }) - - data.domains.forEach((domain) => { - dispatch(setViewsValue(domain.id, domain.statistics.views)) - }) - dispatch(setViewsFetching(false)) - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setViewsFetching(false)) - if (err.name === 'HandledError') return - dispatch(setViewsError(err)) - - } - -}) \ No newline at end of file diff --git a/src/ui/scripts/components/Filter.js b/src/ui/scripts/components/Filter.js index e38f97f4..7eb96a95 100644 --- a/src/ui/scripts/components/Filter.js +++ b/src/ui/scripts/components/Filter.js @@ -161,9 +161,9 @@ const Filter = (props) => { const routesMap = { [ROUTE_VIEWS.key]: [ - createItem(labels.views[props.views.type], [ - createButton('Unique', 'Unique site views', props.setViewsType, props.views.type, views.VIEWS_TYPE_UNIQUE), - createButton('Total', 'Total page views', props.setViewsType, props.views.type, views.VIEWS_TYPE_TOTAL) + createItem(labels.views[props.filter.viewsType], [ + createButton('Unique', 'Unique site views', props.setFilterViewsType, props.filter.viewsType, views.VIEWS_TYPE_UNIQUE), + createButton('Total', 'Total page views', props.setFilterViewsType, props.filter.viewsType, views.VIEWS_TYPE_TOTAL) ]), intervalsItem ], diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 085633e3..13b55ec6 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -1,46 +1,65 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h, Fragment, useEffect, useCallback } from 'react' -import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' -import selectViewsValue from '../../selectors/selectViewsValue' +// import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' +import viewsLoader from '../../loaders/viewsLoader' import enhanceViews from '../../enhancers/enhanceViews' -import mergeViews from '../../utils/mergeViews' +// import mergeViews from '../../utils/mergeViews' import overviewRoute from '../../utils/overviewRoute' import CardViews from '../cards/CardViews' const RouteViews = (props) => { + const createLoader = useCallback((domainId) => { + + return viewsLoader(domainId, { + interval: props.filter.interval, + type: props.filter.viewsType + }) + + }, [ props.filter.interval, props.filter.viewsType ]) + useEffect(() => { - props.fetchViews(props) + return props.domains.value.map( + (domain) => { + const loader = createLoader(domain.id) + props.fetchWidget(props, loader) + } + ) - }, [ props.filter.interval, props.views.type ]) + }, [ props.domains.value, createLoader ]) return ( h(Fragment, {}, - h(CardViews, { - wide: true, - headline: ({ - [VIEWS_TYPE_UNIQUE]: 'Site Views', - [VIEWS_TYPE_TOTAL]: 'Page Views' - })[props.views.type], - interval: props.filter.interval, - loading: props.fetching, - items: mergeViews(props) - }), + // h(CardViews, { + // wide: true, + // headline: ({ + // [VIEWS_TYPE_UNIQUE]: 'Site Views', + // [VIEWS_TYPE_TOTAL]: 'Page Views' + // })[props.filter.viewsType], + // interval: props.filter.interval, + // loading: props.fetching, + // items: mergeViews(props) + // }), props.domains.value.map( - (domain) => ( - h(CardViews, { + (domain) => { + const { id } = createLoader(domain.id) + const widget = props.widgets.value[id] + + if (widget == null) return h('p', { key: domain.id }, 'empty') + + return h(CardViews, { key: domain.id, headline: domain.title, - interval: props.filter.interval, - loading: props.views.fetching, - items: enhanceViews(selectViewsValue(props, domain.id).value, 7), + interval: widget.variables.interval, + loading: widget.fetching, + items: enhanceViews(widget.value, 7), onMore: () => props.setRoute(overviewRoute(domain)) }) - ) + } ) ) diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index a1bc9699..d7e2f9a6 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -7,7 +7,6 @@ export default (state) => { const fetching = ( selectOverviewValue.withoutType(state, ALL_DOMAINS).fetching === true || Object.values(state.overview.value).some((value) => value.fetching) === true || - state.views.fetching === true || state.referrers.fetching === true || state.durations.fetching === true || state.systems.fetching === true || @@ -23,7 +22,6 @@ export default (state) => { const errors = [ selectOverviewValue.withoutType(state, ALL_DOMAINS).error, ...Object.values(state.overview.value).map((value) => value.error), - state.views.error, state.referrers.error, state.durations.error, state.systems.error, diff --git a/src/ui/scripts/index.js b/src/ui/scripts/index.js index d573f08f..02b315cf 100644 --- a/src/ui/scripts/index.js +++ b/src/ui/scripts/index.js @@ -14,7 +14,6 @@ import * as actions from './actions' import { initialState as initialTokenState } from './reducers/token' import { initialState as initialRouteState } from './reducers/route' import { initialState as initialFilterState } from './reducers/filter' -import { initialState as initialViewsState } from './reducers/views' import { initialState as initialSystemsState } from './reducers/systems' import { initialState as initialBrowsersState } from './reducers/browsers' import { initialState as initialDevicesState } from './reducers/devices' @@ -53,11 +52,8 @@ store.subscribe(() => { ...initialFilterState(), sorting: currentState.filter.sorting, range: currentState.filter.range, - interval: currentState.filter.interval - }, - views: { - ...initialViewsState(), - type: currentState.views.type + interval: currentState.filter.interval, + viewsType: currentState.filter.viewsType }, systems: { ...initialSystemsState(), diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js new file mode 100644 index 00000000..e2fe5970 --- /dev/null +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -0,0 +1,35 @@ +export default (domainId, opts) => { + + // TODO: Improve ids + const id = `fetchViews${ domainId }${ JSON.stringify(opts) }` + + const query = ` + query fetchViews($domainId: ID!, $interval: Interval!, $type: ViewType!) { + domain(id: $domainId) { + id + statistics { + views(interval: $interval, type: $type) { + id + count + } + } + } + } + ` + + const variables = { + domainId, + interval: opts.interval, + type: opts.type + } + + const selector = (data) => data.domain.statistics.views + + return { + id, + query, + variables, + selector + } + +} \ No newline at end of file diff --git a/src/ui/scripts/reducers/filter.js b/src/ui/scripts/reducers/filter.js index d1607e37..80b10576 100644 --- a/src/ui/scripts/reducers/filter.js +++ b/src/ui/scripts/reducers/filter.js @@ -3,17 +3,20 @@ import produce from 'immer' import { SET_FILTER_SORTING, SET_FILTER_RANGE, - SET_FILTER_INTERVAL + SET_FILTER_INTERVAL, + SET_FILTER_VIEWS_TYPE } from '../actions' import { SORTINGS_TOP } from '../../../constants/sortings' import { RANGES_LAST_7_DAYS } from '../../../constants/ranges' import { INTERVALS_DAILY } from '../../../constants/intervals' +import { VIEWS_TYPE_UNIQUE } from '../../../constants/views' export const initialState = () => ({ sorting: SORTINGS_TOP, range: RANGES_LAST_7_DAYS, - interval: INTERVALS_DAILY + interval: INTERVALS_DAILY, + viewsType: VIEWS_TYPE_UNIQUE }) export default produce((draft, action) => { @@ -28,6 +31,9 @@ export default produce((draft, action) => { case SET_FILTER_INTERVAL: draft.interval = action.payload break + case SET_FILTER_VIEWS_TYPE: + draft.viewsType = action.payload + break } }, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/index.js b/src/ui/scripts/reducers/index.js index e0807f93..14c22945 100644 --- a/src/ui/scripts/reducers/index.js +++ b/src/ui/scripts/reducers/index.js @@ -12,7 +12,6 @@ import filter from './filter' import domains from './domains' import events from './events' import overview from './overview' -import views from './views' import referrers from './referrers' import durations from './durations' import systems from './systems' @@ -30,7 +29,6 @@ const reducers = combineReducers({ domains, events, overview, - views, referrers, durations, systems, diff --git a/src/ui/scripts/reducers/views.js b/src/ui/scripts/reducers/views.js deleted file mode 100644 index 220f2c20..00000000 --- a/src/ui/scripts/reducers/views.js +++ /dev/null @@ -1,43 +0,0 @@ -import produce from 'immer' - -import { - SET_VIEWS_TYPE, - SET_VIEWS_VALUE, - SET_VIEWS_FETCHING, - SET_VIEWS_ERROR -} from '../actions' - -import { VIEWS_TYPE_UNIQUE } from '../../../constants/views' -import genericState from '../utils/genericState' -import genericSubState from '../utils/genericSubState' - -export const initialState = () => ({ - type: VIEWS_TYPE_UNIQUE, - ...genericState() -}) - -export const initialSubState = genericSubState - -export default produce((draft, action) => { - - const hasDomainId = () => action.domainId != null - const hasDomainValue = () => draft.value[action.domainId] != null - - if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() - - switch (action.type) { - case SET_VIEWS_TYPE: - draft.type = action.payload - break - case SET_VIEWS_VALUE: - draft.value[action.domainId].value = action.payload || initialSubState().value - break - case SET_VIEWS_FETCHING: - draft.fetching = action.payload || initialState().fetching - break - case SET_VIEWS_ERROR: - draft.error = action.payload || initialState().error - break - } - -}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/selectors/selectViewsValue.js b/src/ui/scripts/selectors/selectViewsValue.js index 186907b1..695c38fd 100644 --- a/src/ui/scripts/selectors/selectViewsValue.js +++ b/src/ui/scripts/selectors/selectViewsValue.js @@ -1,6 +1,7 @@ -import { initialSubState } from '../reducers/views' +// TODO: Remove file +// import { initialSubState } from '../reducers/views' -export default (state, domainId) => { - const value = state.views.value[domainId] - return value == null ? initialSubState() : value -} \ No newline at end of file +// export default (state, domainId) => { +// const value = state.views.value[domainId] +// return value == null ? initialSubState() : value +// } \ No newline at end of file From 0275e3f5283886d7f0bcf88b9263376ee682d07a Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 17:12:03 +0100 Subject: [PATCH 034/208] Custom hook to load widget data --- .../components/routes/RouteLanguages.js | 33 +++++------------- .../scripts/components/routes/RoutePages.js | 34 +++++-------------- .../scripts/components/routes/RouteViews.js | 33 +++++------------- src/ui/scripts/utils/useWidgetBundles.js | 27 +++++++++++++++ 4 files changed, 54 insertions(+), 73 deletions(-) create mode 100644 src/ui/scripts/utils/useWidgetBundles.js diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index a2d9e10b..a0c169ae 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -1,40 +1,25 @@ -import { createElement as h, Fragment, useEffect, useCallback } from 'react' +import { createElement as h, Fragment } from 'react' import languagesLoader from '../../loaders/languagesLoader' import enhanceLanguages from '../../enhancers/enhanceLanguages' import overviewRoute from '../../utils/overviewRoute' +import useWidgetBundles from '../../utils/useWidgetBundles' import CardLanguages from '../cards/CardLanguages' const RouteLanguages = (props) => { - const createLoader = useCallback((domainId) => { - - return languagesLoader(domainId, { - range: props.filter.range, - sorting: props.filter.sorting - }) - - }, [ props.filter.range, props.filter.sorting ]) - - useEffect(() => { - - return props.domains.value.map( - (domain) => { - const loader = createLoader(domain.id) - props.fetchWidget(props, loader) - } - ) - - }, [ props.domains.value, createLoader ]) + const widgetBundles = useWidgetBundles(props, languagesLoader, { + range: props.filter.range, + sorting: props.filter.sorting + }) return ( h(Fragment, {}, - props.domains.value.map( - (domain) => { - const { id } = createLoader(domain.id) - const widget = props.widgets.value[id] + widgetBundles.map( + ({ domain, loader }) => { + const widget = props.widgets.value[loader.id] if (widget == null) return h('p', { key: domain.id }, 'empty') diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index 39f3fbec..69ebd84f 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -1,41 +1,25 @@ -import { createElement as h, Fragment, useEffect, useCallback } from 'react' +import { createElement as h, Fragment } from 'react' -// import selectPagesValue from '../../selectors/selectPagesValue' import enhancePages from '../../enhancers/enhancePages' import pagesLoader from '../../loaders/pagesLoader' import overviewRoute from '../../utils/overviewRoute' +import useWidgetBundles from '../../utils/useWidgetBundles' import CardPages from '../cards/CardPages' const RoutePages = (props) => { - const createLoader = useCallback((domainId) => { - - return pagesLoader(domainId, { - range: props.filter.range, - sorting: props.filter.sorting - }) - - }, [ props.filter.range, props.filter.sorting ]) - - useEffect(() => { - - return props.domains.value.map( - (domain) => { - const loader = createLoader(domain.id) - props.fetchWidget(props, loader) - } - ) - - }, [ props.domains.value, createLoader ]) + const widgetBundles = useWidgetBundles(props, pagesLoader, { + range: props.filter.range, + sorting: props.filter.sorting + }) return ( h(Fragment, {}, - props.domains.value.map( - (domain) => { - const { id } = createLoader(domain.id) - const widget = props.widgets.value[id] + widgetBundles.map( + ({ domain, loader }) => { + const widget = props.widgets.value[loader.id] if (widget == null) return h('p', { key: domain.id }, 'empty') diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 13b55ec6..59da740b 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -1,34 +1,20 @@ -import { createElement as h, Fragment, useEffect, useCallback } from 'react' +import { createElement as h, Fragment } from 'react' // import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' import viewsLoader from '../../loaders/viewsLoader' import enhanceViews from '../../enhancers/enhanceViews' // import mergeViews from '../../utils/mergeViews' import overviewRoute from '../../utils/overviewRoute' +import useWidgetBundles from '../../utils/useWidgetBundles' import CardViews from '../cards/CardViews' const RouteViews = (props) => { - const createLoader = useCallback((domainId) => { - - return viewsLoader(domainId, { - interval: props.filter.interval, - type: props.filter.viewsType - }) - - }, [ props.filter.interval, props.filter.viewsType ]) - - useEffect(() => { - - return props.domains.value.map( - (domain) => { - const loader = createLoader(domain.id) - props.fetchWidget(props, loader) - } - ) - - }, [ props.domains.value, createLoader ]) + const widgetBundles = useWidgetBundles(props, viewsLoader, { + interval: props.filter.interval, + type: props.filter.viewsType + }) return ( h(Fragment, {}, @@ -44,10 +30,9 @@ const RouteViews = (props) => { // items: mergeViews(props) // }), - props.domains.value.map( - (domain) => { - const { id } = createLoader(domain.id) - const widget = props.widgets.value[id] + widgetBundles.map( + ({ domain, loader }) => { + const widget = props.widgets.value[loader.id] if (widget == null) return h('p', { key: domain.id }, 'empty') diff --git a/src/ui/scripts/utils/useWidgetBundles.js b/src/ui/scripts/utils/useWidgetBundles.js new file mode 100644 index 00000000..e0a3c6dc --- /dev/null +++ b/src/ui/scripts/utils/useWidgetBundles.js @@ -0,0 +1,27 @@ +import { useEffect, useState } from 'react' + +export default (props, createLoader, opts) => { + + const [ widgetBundles, setWidgetBundles ] = useState([]) + + useEffect(() => { + + const widgetBundles = props.domains.value.map( + (domain) => { + const loader = createLoader(domain.id, opts) + props.fetchWidget(props, loader) + + return { + domain, + loader + } + } + ) + + setWidgetBundles(widgetBundles) + + }, [ props.domains.value, ...Object.values(opts) ]) + + return widgetBundles + +} \ No newline at end of file From ccf1f476ae912f09c9a3575179b154609f7641a7 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 17:25:17 +0100 Subject: [PATCH 035/208] Refactor devices to use widget loading and include loading and error states of widgets in enhanceState --- src/ui/scripts/actions/devices.js | 75 ------------------- src/ui/scripts/actions/filter.js | 6 ++ src/ui/scripts/actions/index.js | 1 - src/ui/scripts/components/Filter.js | 4 +- .../scripts/components/routes/RouteDevices.js | 35 +++++---- src/ui/scripts/enhancers/enhanceState.js | 4 +- src/ui/scripts/index.js | 8 +- src/ui/scripts/loaders/devicesLoader.js | 37 +++++++++ src/ui/scripts/reducers/devices.js | 46 ------------ src/ui/scripts/reducers/filter.js | 10 ++- src/ui/scripts/reducers/index.js | 2 - .../scripts/selectors/selectDevicesValue.js | 6 -- 12 files changed, 77 insertions(+), 157 deletions(-) delete mode 100644 src/ui/scripts/actions/devices.js create mode 100644 src/ui/scripts/loaders/devicesLoader.js delete mode 100644 src/ui/scripts/reducers/devices.js delete mode 100644 src/ui/scripts/selectors/selectDevicesValue.js diff --git a/src/ui/scripts/actions/devices.js b/src/ui/scripts/actions/devices.js deleted file mode 100644 index 2c60f642..00000000 --- a/src/ui/scripts/actions/devices.js +++ /dev/null @@ -1,75 +0,0 @@ -import api from '../utils/api' -import signalHandler from '../utils/signalHandler' - -export const SET_DEVICES_TYPE = Symbol() -export const SET_DEVICES_VALUE = Symbol() -export const SET_DEVICES_FETCHING = Symbol() -export const SET_DEVICES_ERROR = Symbol() - -export const setDevicesType = (payload) => ({ - type: SET_DEVICES_TYPE, - payload -}) - -export const setDevicesValue = (domainId, payload) => ({ - type: SET_DEVICES_VALUE, - domainId, - payload -}) - -export const setDevicesFetching = (payload) => ({ - type: SET_DEVICES_FETCHING, - payload -}) - -export const setDevicesError = (payload) => ({ - type: SET_DEVICES_ERROR, - payload -}) - -export const fetchDevices = signalHandler((signal) => (props) => async (dispatch) => { - - dispatch(setDevicesFetching(true)) - dispatch(setDevicesError()) - - try { - - const data = await api({ - query: ` - query fetchDevices($sorting: Sorting!, $type: DeviceType!, $range: Range) { - domains { - id - statistics { - devices(sorting: $sorting, type: $type, range: $range) { - id - count - created - } - } - } - } - `, - variables: { - sorting: props.filter.sorting, - type: props.devices.type, - range: props.filter.range - }, - props, - signal: signal() - }) - - data.domains.forEach((domain) => { - dispatch(setDevicesValue(domain.id, domain.statistics.devices)) - }) - dispatch(setDevicesFetching(false)) - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setDevicesFetching(false)) - if (err.name === 'HandledError') return - dispatch(setDevicesError(err)) - - } - -}) \ No newline at end of file diff --git a/src/ui/scripts/actions/filter.js b/src/ui/scripts/actions/filter.js index 5a00f4a7..2e179f5a 100644 --- a/src/ui/scripts/actions/filter.js +++ b/src/ui/scripts/actions/filter.js @@ -2,6 +2,7 @@ export const SET_FILTER_SORTING = Symbol() export const SET_FILTER_RANGE = Symbol() export const SET_FILTER_INTERVAL = Symbol() export const SET_FILTER_VIEWS_TYPE = Symbol() +export const SET_FILTER_DEVICES_TYPE = Symbol() export const setFilterSorting = (payload) => ({ type: SET_FILTER_SORTING, @@ -21,4 +22,9 @@ export const setFilterInterval = (payload) => ({ export const setFilterViewsType = (payload) => ({ type: SET_FILTER_VIEWS_TYPE, payload +}) + +export const setFilterDevicesType = (payload) => ({ + type: SET_FILTER_DEVICES_TYPE, + payload }) \ No newline at end of file diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index d50ed09d..83691f37 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -10,7 +10,6 @@ export * from './referrers' export * from './durations' export * from './sizes' export * from './systems' -export * from './devices' export * from './browsers' export * from './widgets' diff --git a/src/ui/scripts/components/Filter.js b/src/ui/scripts/components/Filter.js index 7eb96a95..2f6a25ff 100644 --- a/src/ui/scripts/components/Filter.js +++ b/src/ui/scripts/components/Filter.js @@ -194,8 +194,8 @@ const Filter = (props) => { ...sortingButtons, createSeparator(), onlyInactiveButton( - createButton('Show model', 'Include device model', props.setDevicesType, props.devices.type, devices.DEVICES_TYPE_WITH_MODEL), - createButton('Hide model', 'Don\'t include model', props.setDevicesType, props.devices.type, devices.DEVICES_TYPE_NO_MODEL) + createButton('Show model', 'Include device model', props.setFilterDevicesType, props.filter.devicesType, devices.DEVICES_TYPE_WITH_MODEL), + createButton('Hide model', 'Don\'t include model', props.setFilterDevicesType, props.filter.devicesType, devices.DEVICES_TYPE_NO_MODEL) ) ]), rangeItem diff --git a/src/ui/scripts/components/routes/RouteDevices.js b/src/ui/scripts/components/routes/RouteDevices.js index 080b6a47..ef011adb 100644 --- a/src/ui/scripts/components/routes/RouteDevices.js +++ b/src/ui/scripts/components/routes/RouteDevices.js @@ -1,34 +1,39 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h, Fragment } from 'react' -import selectDevicesValue from '../../selectors/selectDevicesValue' +import devicesLoader from '../../loaders/devicesLoader' import enhanceDevices from '../../enhancers/enhanceDevices' import overviewRoute from '../../utils/overviewRoute' +import useWidgetBundles from '../../utils/useWidgetBundles' import CardDevices from '../cards/CardDevices' const RouteDevices = (props) => { - useEffect(() => { - - props.fetchDevices(props) - - }, [ props.filter.range, props.filter.sorting, props.devices.type ]) + const widgetBundles = useWidgetBundles(props, devicesLoader, { + range: props.filter.range, + sorting: props.filter.sorting, + type: props.filter.devicesType + }) return ( h(Fragment, {}, - props.domains.value.map( - (domain) => ( - h(CardDevices, { + widgetBundles.map( + ({ domain, loader }) => { + const widget = props.widgets.value[loader.id] + + if (widget == null) return h('p', { key: domain.id }, 'empty') + + return h(CardDevices, { key: domain.id, headline: domain.title, - range: props.filter.range, - sorting: props.filter.sorting, - loading: props.devices.fetching, - items: enhanceDevices(selectDevicesValue(props, domain.id).value), + range: widget.variables.range, + sorting: widget.variables.sorting, + loading: widget.fetching, + items: enhanceDevices(widget.value), onMore: () => props.setRoute(overviewRoute(domain)) }) - ) + } ) ) diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index d7e2f9a6..56a87c31 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -7,10 +7,10 @@ export default (state) => { const fetching = ( selectOverviewValue.withoutType(state, ALL_DOMAINS).fetching === true || Object.values(state.overview.value).some((value) => value.fetching) === true || + Object.values(state.widgets.value).some((value) => value.fetching) === true || state.referrers.fetching === true || state.durations.fetching === true || state.systems.fetching === true || - state.devices.fetching === true || state.browsers.fetching === true || state.sizes.fetching === true || state.domains.fetching === true || @@ -22,10 +22,10 @@ export default (state) => { const errors = [ selectOverviewValue.withoutType(state, ALL_DOMAINS).error, ...Object.values(state.overview.value).map((value) => value.error), + ...Object.values(state.widgets.value).map((value) => value.error), state.referrers.error, state.durations.error, state.systems.error, - state.devices.error, state.browsers.error, state.sizes.error, state.domains.error, diff --git a/src/ui/scripts/index.js b/src/ui/scripts/index.js index 02b315cf..2ce1f785 100644 --- a/src/ui/scripts/index.js +++ b/src/ui/scripts/index.js @@ -16,7 +16,6 @@ import { initialState as initialRouteState } from './reducers/route' import { initialState as initialFilterState } from './reducers/filter' import { initialState as initialSystemsState } from './reducers/systems' import { initialState as initialBrowsersState } from './reducers/browsers' -import { initialState as initialDevicesState } from './reducers/devices' import { initialState as initialSizesState } from './reducers/sizes' import Main from './components/Main' @@ -53,7 +52,8 @@ store.subscribe(() => { sorting: currentState.filter.sorting, range: currentState.filter.range, interval: currentState.filter.interval, - viewsType: currentState.filter.viewsType + viewsType: currentState.filter.viewsType, + devicesType: currentState.filter.devicesType }, systems: { ...initialSystemsState(), @@ -63,10 +63,6 @@ store.subscribe(() => { ...initialBrowsersState(), type: currentState.browsers.type }, - devices: { - ...initialDevicesState(), - type: currentState.devices.type - }, sizes: { ...initialSizesState(), type: currentState.sizes.type diff --git a/src/ui/scripts/loaders/devicesLoader.js b/src/ui/scripts/loaders/devicesLoader.js new file mode 100644 index 00000000..5aa7f4ab --- /dev/null +++ b/src/ui/scripts/loaders/devicesLoader.js @@ -0,0 +1,37 @@ +export default (domainId, opts) => { + + // TODO: Improve ids + const id = `fetchDevices${ domainId }${ JSON.stringify(opts) }` + + const query = ` + query fetchDevices($domainId: ID!, $sorting: Sorting!, $type: DeviceType!, $range: Range) { + domain(id: $domainId) { + id + statistics { + devices(sorting: $sorting, type: $type, range: $range) { + id + count + created + } + } + } + } + ` + + const variables = { + domainId, + sorting: opts.sorting, + range: opts.range, + type: opts.type + } + + const selector = (data) => data.domain.statistics.devices + + return { + id, + query, + variables, + selector + } + +} \ No newline at end of file diff --git a/src/ui/scripts/reducers/devices.js b/src/ui/scripts/reducers/devices.js deleted file mode 100644 index 3aa51643..00000000 --- a/src/ui/scripts/reducers/devices.js +++ /dev/null @@ -1,46 +0,0 @@ -import produce from 'immer' - -import { - SET_DEVICES_ERROR, - SET_DEVICES_FETCHING, - SET_DEVICES_VALUE, - SET_DEVICES_TYPE -} from '../actions' - -import { DEVICES_TYPE_WITH_MODEL } from '../../../constants/devices' -import genericState from '../utils/genericState' -import genericSubState from '../utils/genericSubState' - -export const initialState = () => ({ - type: DEVICES_TYPE_WITH_MODEL, - ...genericState() -}) - -export const initialSubState = genericSubState - -export default produce((draft, action) => { - - const hasDomainId = () => action.domainId != null - const hasDomainValue = () => draft.value[action.domainId] != null - - if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() - - switch (action.type) { - case SET_DEVICES_TYPE: - if (draft.type === action.payload) break - // Reset value because the view shouldn't show the old data when switching - draft.value = initialState().value - draft.type = action.payload - break - case SET_DEVICES_VALUE: - draft.value[action.domainId].value = action.payload || initialSubState().value - break - case SET_DEVICES_FETCHING: - draft.fetching = action.payload || initialState().fetching - break - case SET_DEVICES_ERROR: - draft.error = action.payload || initialState().error - break - } - -}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/filter.js b/src/ui/scripts/reducers/filter.js index 80b10576..db4dc892 100644 --- a/src/ui/scripts/reducers/filter.js +++ b/src/ui/scripts/reducers/filter.js @@ -4,19 +4,22 @@ import { SET_FILTER_SORTING, SET_FILTER_RANGE, SET_FILTER_INTERVAL, - SET_FILTER_VIEWS_TYPE + SET_FILTER_VIEWS_TYPE, + SET_FILTER_DEVICES_TYPE } from '../actions' import { SORTINGS_TOP } from '../../../constants/sortings' import { RANGES_LAST_7_DAYS } from '../../../constants/ranges' import { INTERVALS_DAILY } from '../../../constants/intervals' import { VIEWS_TYPE_UNIQUE } from '../../../constants/views' +import { DEVICES_TYPE_WITH_MODEL } from '../../../constants/devices' export const initialState = () => ({ sorting: SORTINGS_TOP, range: RANGES_LAST_7_DAYS, interval: INTERVALS_DAILY, - viewsType: VIEWS_TYPE_UNIQUE + viewsType: VIEWS_TYPE_UNIQUE, + devicesType: DEVICES_TYPE_WITH_MODEL }) export default produce((draft, action) => { @@ -34,6 +37,9 @@ export default produce((draft, action) => { case SET_FILTER_VIEWS_TYPE: draft.viewsType = action.payload break + case SET_FILTER_DEVICES_TYPE: + draft.devicesType = action.payload + break } }, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/index.js b/src/ui/scripts/reducers/index.js index 14c22945..d218846d 100644 --- a/src/ui/scripts/reducers/index.js +++ b/src/ui/scripts/reducers/index.js @@ -15,7 +15,6 @@ import overview from './overview' import referrers from './referrers' import durations from './durations' import systems from './systems' -import devices from './devices' import browsers from './browsers' import sizes from './sizes' import widgets from './widgets' @@ -32,7 +31,6 @@ const reducers = combineReducers({ referrers, durations, systems, - devices, browsers, sizes, widgets diff --git a/src/ui/scripts/selectors/selectDevicesValue.js b/src/ui/scripts/selectors/selectDevicesValue.js deleted file mode 100644 index 8c585441..00000000 --- a/src/ui/scripts/selectors/selectDevicesValue.js +++ /dev/null @@ -1,6 +0,0 @@ -import { initialSubState } from '../reducers/devices' - -export default (state, domainId) => { - const value = state.devices.value[domainId] - return value == null ? initialSubState() : value -} \ No newline at end of file From 72f7ee9aad500d8fc790508ff3df2ae8164f233f Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 17:35:38 +0100 Subject: [PATCH 036/208] Refactor browsers to use widgets loader --- src/ui/scripts/actions/browsers.js | 75 ------------------- src/ui/scripts/actions/filter.js | 6 ++ src/ui/scripts/actions/index.js | 1 - src/ui/scripts/components/Filter.js | 4 +- .../components/routes/RouteBrowsers.js | 35 +++++---- src/ui/scripts/enhancers/enhanceState.js | 2 - src/ui/scripts/index.js | 8 +- src/ui/scripts/loaders/browsersLoader.js | 37 +++++++++ src/ui/scripts/reducers/browsers.js | 46 ------------ src/ui/scripts/reducers/filter.js | 10 ++- src/ui/scripts/reducers/index.js | 2 - .../scripts/selectors/selectBrowsersValue.js | 6 -- 12 files changed, 75 insertions(+), 157 deletions(-) delete mode 100644 src/ui/scripts/actions/browsers.js create mode 100644 src/ui/scripts/loaders/browsersLoader.js delete mode 100644 src/ui/scripts/reducers/browsers.js delete mode 100644 src/ui/scripts/selectors/selectBrowsersValue.js diff --git a/src/ui/scripts/actions/browsers.js b/src/ui/scripts/actions/browsers.js deleted file mode 100644 index d28956a0..00000000 --- a/src/ui/scripts/actions/browsers.js +++ /dev/null @@ -1,75 +0,0 @@ -import api from '../utils/api' -import signalHandler from '../utils/signalHandler' - -export const SET_BROWSERS_TYPE = Symbol() -export const SET_BROWSERS_VALUE = Symbol() -export const SET_BROWSERS_FETCHING = Symbol() -export const SET_BROWSERS_ERROR = Symbol() - -export const setBrowsersType = (payload) => ({ - type: SET_BROWSERS_TYPE, - payload -}) - -export const setBrowsersValue = (domainId, payload) => ({ - type: SET_BROWSERS_VALUE, - domainId, - payload -}) - -export const setBrowsersFetching = (payload) => ({ - type: SET_BROWSERS_FETCHING, - payload -}) - -export const setBrowsersError = (payload) => ({ - type: SET_BROWSERS_ERROR, - payload -}) - -export const fetchBrowsers = signalHandler((signal) => (props) => async (dispatch) => { - - dispatch(setBrowsersFetching(true)) - dispatch(setBrowsersError()) - - try { - - const data = await api({ - query: ` - query fetchBrowsers($sorting: Sorting!, $type: BrowserType!, $range: Range) { - domains { - id - statistics { - browsers(sorting: $sorting, type: $type, range: $range) { - id - count - created - } - } - } - } - `, - variables: { - sorting: props.filter.sorting, - type: props.browsers.type, - range: props.filter.range - }, - props, - signal: signal() - }) - - data.domains.forEach((domain) => { - dispatch(setBrowsersValue(domain.id, domain.statistics.browsers)) - }) - dispatch(setBrowsersFetching(false)) - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setBrowsersFetching(false)) - if (err.name === 'HandledError') return - dispatch(setBrowsersError(err)) - - } - -}) \ No newline at end of file diff --git a/src/ui/scripts/actions/filter.js b/src/ui/scripts/actions/filter.js index 2e179f5a..673887b1 100644 --- a/src/ui/scripts/actions/filter.js +++ b/src/ui/scripts/actions/filter.js @@ -3,6 +3,7 @@ export const SET_FILTER_RANGE = Symbol() export const SET_FILTER_INTERVAL = Symbol() export const SET_FILTER_VIEWS_TYPE = Symbol() export const SET_FILTER_DEVICES_TYPE = Symbol() +export const SET_FILTER_BROWSERS_TYPE = Symbol() export const setFilterSorting = (payload) => ({ type: SET_FILTER_SORTING, @@ -27,4 +28,9 @@ export const setFilterViewsType = (payload) => ({ export const setFilterDevicesType = (payload) => ({ type: SET_FILTER_DEVICES_TYPE, payload +}) + +export const setFilterBrowsersType = (payload) => ({ + type: SET_FILTER_BROWSERS_TYPE, + payload }) \ No newline at end of file diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index 83691f37..388ac0b0 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -10,7 +10,6 @@ export * from './referrers' export * from './durations' export * from './sizes' export * from './systems' -export * from './browsers' export * from './widgets' export const RESET_STATE = Symbol() diff --git a/src/ui/scripts/components/Filter.js b/src/ui/scripts/components/Filter.js index 2f6a25ff..7506530b 100644 --- a/src/ui/scripts/components/Filter.js +++ b/src/ui/scripts/components/Filter.js @@ -205,8 +205,8 @@ const Filter = (props) => { ...sortingButtons, createSeparator(), onlyInactiveButton( - createButton('Show version', 'Include browser version', props.setBrowsersType, props.browsers.type, browsers.BROWSERS_TYPE_WITH_VERSION), - createButton('Hide version', 'Don\'t include version', props.setBrowsersType, props.browsers.type, browsers.BROWSERS_TYPE_NO_VERSION) + createButton('Show version', 'Include browser version', props.setFilterBrowsersType, props.filter.browsersType, browsers.BROWSERS_TYPE_WITH_VERSION), + createButton('Hide version', 'Don\'t include version', props.setFilterBrowsersType, props.filter.browsersType, browsers.BROWSERS_TYPE_NO_VERSION) ) ]), rangeItem diff --git a/src/ui/scripts/components/routes/RouteBrowsers.js b/src/ui/scripts/components/routes/RouteBrowsers.js index cdac9b36..0b44f5c1 100644 --- a/src/ui/scripts/components/routes/RouteBrowsers.js +++ b/src/ui/scripts/components/routes/RouteBrowsers.js @@ -1,34 +1,39 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h, Fragment } from 'react' -import selectBrowsersValue from '../../selectors/selectBrowsersValue' +import browsersLoader from '../../loaders/browsersLoader' import enhanceBrowsers from '../../enhancers/enhanceBrowsers' import overviewRoute from '../../utils/overviewRoute' +import useWidgetBundles from '../../utils/useWidgetBundles' import CardBrowsers from '../cards/CardBrowsers' const RouteBrowsers = (props) => { - useEffect(() => { - - props.fetchBrowsers(props) - - }, [ props.filter.range, props.filter.sorting, props.browsers.type ]) + const widgetBundles = useWidgetBundles(props, browsersLoader, { + range: props.filter.range, + sorting: props.filter.sorting, + type: props.filter.browsersType + }) return ( h(Fragment, {}, - props.domains.value.map( - (domain) => ( - h(CardBrowsers, { + widgetBundles.map( + ({ domain, loader }) => { + const widget = props.widgets.value[loader.id] + + if (widget == null) return h('p', { key: domain.id }, 'empty') + + return h(CardBrowsers, { key: domain.id, headline: domain.title, - range: props.filter.range, - sorting: props.filter.sorting, - loading: props.browsers.fetching, - items: enhanceBrowsers(selectBrowsersValue(props, domain.id).value), + range: widget.variables.range, + sorting: widget.variables.sorting, + loading: widget.fetching, + items: enhanceBrowsers(widget.value), onMore: () => props.setRoute(overviewRoute(domain)) }) - ) + } ) ) diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index 56a87c31..ecca6ea0 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -11,7 +11,6 @@ export default (state) => { state.referrers.fetching === true || state.durations.fetching === true || state.systems.fetching === true || - state.browsers.fetching === true || state.sizes.fetching === true || state.domains.fetching === true || state.token.fetching === true || @@ -26,7 +25,6 @@ export default (state) => { state.referrers.error, state.durations.error, state.systems.error, - state.browsers.error, state.sizes.error, state.domains.error, state.token.error, diff --git a/src/ui/scripts/index.js b/src/ui/scripts/index.js index 2ce1f785..8515fccc 100644 --- a/src/ui/scripts/index.js +++ b/src/ui/scripts/index.js @@ -15,7 +15,6 @@ import { initialState as initialTokenState } from './reducers/token' import { initialState as initialRouteState } from './reducers/route' import { initialState as initialFilterState } from './reducers/filter' import { initialState as initialSystemsState } from './reducers/systems' -import { initialState as initialBrowsersState } from './reducers/browsers' import { initialState as initialSizesState } from './reducers/sizes' import Main from './components/Main' @@ -53,16 +52,13 @@ store.subscribe(() => { range: currentState.filter.range, interval: currentState.filter.interval, viewsType: currentState.filter.viewsType, - devicesType: currentState.filter.devicesType + devicesType: currentState.filter.devicesType, + browsersType: currentState.filter.browsersType }, systems: { ...initialSystemsState(), type: currentState.systems.type }, - browsers: { - ...initialBrowsersState(), - type: currentState.browsers.type - }, sizes: { ...initialSizesState(), type: currentState.sizes.type diff --git a/src/ui/scripts/loaders/browsersLoader.js b/src/ui/scripts/loaders/browsersLoader.js new file mode 100644 index 00000000..854a5471 --- /dev/null +++ b/src/ui/scripts/loaders/browsersLoader.js @@ -0,0 +1,37 @@ +export default (domainId, opts) => { + + // TODO: Improve ids + const id = `fetchBrowsers${ domainId }${ JSON.stringify(opts) }` + + const query = ` + query fetchBrowsers($domainId: ID!, $sorting: Sorting!, $type: BrowserType!, $range: Range) { + domain(id: $domainId) { + id + statistics { + browsers(sorting: $sorting, type: $type, range: $range) { + id + count + created + } + } + } + } + ` + + const variables = { + domainId, + sorting: opts.sorting, + range: opts.range, + type: opts.type + } + + const selector = (data) => data.domain.statistics.browsers + + return { + id, + query, + variables, + selector + } + +} \ No newline at end of file diff --git a/src/ui/scripts/reducers/browsers.js b/src/ui/scripts/reducers/browsers.js deleted file mode 100644 index 52c0f0c9..00000000 --- a/src/ui/scripts/reducers/browsers.js +++ /dev/null @@ -1,46 +0,0 @@ -import produce from 'immer' - -import { - SET_BROWSERS_ERROR, - SET_BROWSERS_FETCHING, - SET_BROWSERS_VALUE, - SET_BROWSERS_TYPE -} from '../actions' - -import { BROWSERS_TYPE_NO_VERSION } from '../../../constants/browsers' -import genericState from '../utils/genericState' -import genericSubState from '../utils/genericSubState' - -export const initialState = () => ({ - type: BROWSERS_TYPE_NO_VERSION, - ...genericState() -}) - -export const initialSubState = genericSubState - -export default produce((draft, action) => { - - const hasDomainId = () => action.domainId != null - const hasDomainValue = () => draft.value[action.domainId] != null - - if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() - - switch (action.type) { - case SET_BROWSERS_TYPE: - if (draft.type === action.payload) break - // Reset value because the view shouldn't show the old data when switching - draft.value = initialState().value - draft.type = action.payload - break - case SET_BROWSERS_VALUE: - draft.value[action.domainId].value = action.payload || initialSubState().value - break - case SET_BROWSERS_FETCHING: - draft.fetching = action.payload || initialState().fetching - break - case SET_BROWSERS_ERROR: - draft.error = action.payload || initialState().error - break - } - -}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/filter.js b/src/ui/scripts/reducers/filter.js index db4dc892..18bd1259 100644 --- a/src/ui/scripts/reducers/filter.js +++ b/src/ui/scripts/reducers/filter.js @@ -5,7 +5,8 @@ import { SET_FILTER_RANGE, SET_FILTER_INTERVAL, SET_FILTER_VIEWS_TYPE, - SET_FILTER_DEVICES_TYPE + SET_FILTER_DEVICES_TYPE, + SET_FILTER_BROWSERS_TYPE } from '../actions' import { SORTINGS_TOP } from '../../../constants/sortings' @@ -13,13 +14,15 @@ import { RANGES_LAST_7_DAYS } from '../../../constants/ranges' import { INTERVALS_DAILY } from '../../../constants/intervals' import { VIEWS_TYPE_UNIQUE } from '../../../constants/views' import { DEVICES_TYPE_WITH_MODEL } from '../../../constants/devices' +import { BROWSERS_TYPE_WITH_VERSION } from '../../../constants/browsers' export const initialState = () => ({ sorting: SORTINGS_TOP, range: RANGES_LAST_7_DAYS, interval: INTERVALS_DAILY, viewsType: VIEWS_TYPE_UNIQUE, - devicesType: DEVICES_TYPE_WITH_MODEL + devicesType: DEVICES_TYPE_WITH_MODEL, + browsersType: BROWSERS_TYPE_WITH_VERSION }) export default produce((draft, action) => { @@ -40,6 +43,9 @@ export default produce((draft, action) => { case SET_FILTER_DEVICES_TYPE: draft.devicesType = action.payload break + case SET_FILTER_BROWSERS_TYPE: + draft.browsersType = action.payload + break } }, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/index.js b/src/ui/scripts/reducers/index.js index d218846d..6b81880f 100644 --- a/src/ui/scripts/reducers/index.js +++ b/src/ui/scripts/reducers/index.js @@ -15,7 +15,6 @@ import overview from './overview' import referrers from './referrers' import durations from './durations' import systems from './systems' -import browsers from './browsers' import sizes from './sizes' import widgets from './widgets' @@ -31,7 +30,6 @@ const reducers = combineReducers({ referrers, durations, systems, - browsers, sizes, widgets }) diff --git a/src/ui/scripts/selectors/selectBrowsersValue.js b/src/ui/scripts/selectors/selectBrowsersValue.js deleted file mode 100644 index 88e8ccef..00000000 --- a/src/ui/scripts/selectors/selectBrowsersValue.js +++ /dev/null @@ -1,6 +0,0 @@ -import { initialSubState } from '../reducers/browsers' - -export default (state, domainId) => { - const value = state.browsers.value[domainId] - return value == null ? initialSubState() : value -} \ No newline at end of file From 78563a510e35f6f93163397b29689559340afbe3 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 18:20:47 +0100 Subject: [PATCH 037/208] Refactor referrers to use widget loading --- src/ui/scripts/actions/index.js | 1 - src/ui/scripts/actions/referrers.js | 68 ------------------- .../components/routes/RouteReferrers.js | 34 ++++++---- src/ui/scripts/enhancers/enhanceState.js | 2 - src/ui/scripts/loaders/referrersLoader.js | 36 ++++++++++ src/ui/scripts/reducers/index.js | 2 - src/ui/scripts/reducers/referrers.js | 35 ---------- .../scripts/selectors/selectReferrersValue.js | 6 -- 8 files changed, 55 insertions(+), 129 deletions(-) delete mode 100644 src/ui/scripts/actions/referrers.js create mode 100644 src/ui/scripts/loaders/referrersLoader.js delete mode 100644 src/ui/scripts/reducers/referrers.js delete mode 100644 src/ui/scripts/selectors/selectReferrersValue.js diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index 388ac0b0..db2a7836 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -6,7 +6,6 @@ export * from './filter' export * from './domains' export * from './events' export * from './overview' -export * from './referrers' export * from './durations' export * from './sizes' export * from './systems' diff --git a/src/ui/scripts/actions/referrers.js b/src/ui/scripts/actions/referrers.js deleted file mode 100644 index 23b89e08..00000000 --- a/src/ui/scripts/actions/referrers.js +++ /dev/null @@ -1,68 +0,0 @@ -import api from '../utils/api' -import signalHandler from '../utils/signalHandler' - -export const SET_REFERRERS_VALUE = Symbol() -export const SET_REFERRERS_FETCHING = Symbol() -export const SET_REFERRERS_ERROR = Symbol() - -export const setReferrersValue = (domainId, payload) => ({ - type: SET_REFERRERS_VALUE, - domainId, - payload -}) - -export const setReferrersFetching = (payload) => ({ - type: SET_REFERRERS_FETCHING, - payload -}) - -export const setReferrersError = (payload) => ({ - type: SET_REFERRERS_ERROR, - payload -}) - -export const fetchReferrers = signalHandler((signal) => (props) => async (dispatch) => { - - dispatch(setReferrersFetching(true)) - dispatch(setReferrersError()) - - try { - - const data = await api({ - query: ` - query fetchReferrers($sorting: Sorting!, $range: Range) { - domains { - id - statistics { - referrers(sorting: $sorting, range: $range) { - id - count - created - } - } - } - } - `, - variables: { - sorting: props.filter.sorting, - range: props.filter.range - }, - props, - signal: signal() - }) - - data.domains.forEach((domain) => { - dispatch(setReferrersValue(domain.id, domain.statistics.referrers)) - }) - dispatch(setReferrersFetching(false)) - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setReferrersFetching(false)) - if (err.name === 'HandledError') return - dispatch(setReferrersError(err)) - - } - -}) \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteReferrers.js b/src/ui/scripts/components/routes/RouteReferrers.js index dd68f88b..80b9e0a8 100644 --- a/src/ui/scripts/components/routes/RouteReferrers.js +++ b/src/ui/scripts/components/routes/RouteReferrers.js @@ -1,34 +1,38 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h, Fragment } from 'react' -import selectReferrersValue from '../../selectors/selectReferrersValue' +import referrersLoader from '../../loaders/referrersLoader' import enhanceReferrers from '../../enhancers/enhanceReferrers' import overviewRoute from '../../utils/overviewRoute' +import useWidgetBundles from '../../utils/useWidgetBundles' import CardReferrers from '../cards/CardReferrers' const RouteReferrers = (props) => { - useEffect(() => { - - props.fetchReferrers(props) - - }, [ props.filter.range, props.filter.sorting ]) + const widgetBundles = useWidgetBundles(props, referrersLoader, { + range: props.filter.range, + sorting: props.filter.sorting + }) return ( h(Fragment, {}, - props.domains.value.map( - (domain) => ( - h(CardReferrers, { + widgetBundles.map( + ({ domain, loader }) => { + const widget = props.widgets.value[loader.id] + + if (widget == null) return h('p', { key: domain.id }, 'empty') + + return h(CardReferrers, { key: domain.id, headline: domain.title, - range: props.filter.range, - sorting: props.filter.sorting, - loading: props.referrers.fetching, - items: enhanceReferrers(selectReferrersValue(props, domain.id).value), + range: widget.variables.range, + sorting: widget.variables.sorting, + loading: widget.fetching, + items: enhanceReferrers(widget.value), onMore: () => props.setRoute(overviewRoute(domain)) }) - ) + } ) ) diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index ecca6ea0..5875fef0 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -8,7 +8,6 @@ export default (state) => { selectOverviewValue.withoutType(state, ALL_DOMAINS).fetching === true || Object.values(state.overview.value).some((value) => value.fetching) === true || Object.values(state.widgets.value).some((value) => value.fetching) === true || - state.referrers.fetching === true || state.durations.fetching === true || state.systems.fetching === true || state.sizes.fetching === true || @@ -22,7 +21,6 @@ export default (state) => { selectOverviewValue.withoutType(state, ALL_DOMAINS).error, ...Object.values(state.overview.value).map((value) => value.error), ...Object.values(state.widgets.value).map((value) => value.error), - state.referrers.error, state.durations.error, state.systems.error, state.sizes.error, diff --git a/src/ui/scripts/loaders/referrersLoader.js b/src/ui/scripts/loaders/referrersLoader.js new file mode 100644 index 00000000..3001fb26 --- /dev/null +++ b/src/ui/scripts/loaders/referrersLoader.js @@ -0,0 +1,36 @@ +export default (domainId, opts) => { + + // TODO: Improve ids + const id = `fetchReferrers${ domainId }${ JSON.stringify(opts) }` + + const query = ` + query fetchReferrers($domainId: ID!, $sorting: Sorting!, $range: Range) { + domain(id: $domainId) { + id + statistics { + referrers(sorting: $sorting, range: $range) { + id + count + created + } + } + } + } + ` + + const variables = { + domainId, + sorting: opts.sorting, + range: opts.range + } + + const selector = (data) => data.domain.statistics.referrers + + return { + id, + query, + variables, + selector + } + +} \ No newline at end of file diff --git a/src/ui/scripts/reducers/index.js b/src/ui/scripts/reducers/index.js index 6b81880f..47d6e9aa 100644 --- a/src/ui/scripts/reducers/index.js +++ b/src/ui/scripts/reducers/index.js @@ -12,7 +12,6 @@ import filter from './filter' import domains from './domains' import events from './events' import overview from './overview' -import referrers from './referrers' import durations from './durations' import systems from './systems' import sizes from './sizes' @@ -27,7 +26,6 @@ const reducers = combineReducers({ domains, events, overview, - referrers, durations, systems, sizes, diff --git a/src/ui/scripts/reducers/referrers.js b/src/ui/scripts/reducers/referrers.js deleted file mode 100644 index 46a68167..00000000 --- a/src/ui/scripts/reducers/referrers.js +++ /dev/null @@ -1,35 +0,0 @@ -import produce from 'immer' - -import { - SET_REFERRERS_VALUE, - SET_REFERRERS_FETCHING, - SET_REFERRERS_ERROR -} from '../actions' - -import genericState from '../utils/genericState' -import genericSubState from '../utils/genericSubState' - -export const initialState = genericState - -export const initialSubState = genericSubState - -export default produce((draft, action) => { - - const hasDomainId = () => action.domainId != null - const hasDomainValue = () => draft.value[action.domainId] != null - - if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() - - switch (action.type) { - case SET_REFERRERS_VALUE: - draft.value[action.domainId].value = action.payload || initialSubState().value - break - case SET_REFERRERS_FETCHING: - draft.fetching = action.payload || initialState().fetching - break - case SET_REFERRERS_ERROR: - draft.error = action.payload || initialState().error - break - } - -}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/selectors/selectReferrersValue.js b/src/ui/scripts/selectors/selectReferrersValue.js deleted file mode 100644 index 41863f65..00000000 --- a/src/ui/scripts/selectors/selectReferrersValue.js +++ /dev/null @@ -1,6 +0,0 @@ -import { initialSubState } from '../reducers/referrers' - -export default (state, domainId) => { - const value = state.referrers.value[domainId] - return value == null ? initialSubState() : value -} \ No newline at end of file From 8300b899ad43fc6ffe7f8c9dd095a18111e2251e Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 18:23:01 +0100 Subject: [PATCH 038/208] Only pass widgetId around --- src/ui/scripts/components/routes/RouteBrowsers.js | 4 ++-- src/ui/scripts/components/routes/RouteDevices.js | 4 ++-- src/ui/scripts/components/routes/RouteLanguages.js | 4 ++-- src/ui/scripts/components/routes/RoutePages.js | 4 ++-- src/ui/scripts/components/routes/RouteReferrers.js | 4 ++-- src/ui/scripts/components/routes/RouteViews.js | 4 ++-- src/ui/scripts/utils/useWidgetBundles.js | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/ui/scripts/components/routes/RouteBrowsers.js b/src/ui/scripts/components/routes/RouteBrowsers.js index 0b44f5c1..3d21f7a9 100644 --- a/src/ui/scripts/components/routes/RouteBrowsers.js +++ b/src/ui/scripts/components/routes/RouteBrowsers.js @@ -19,8 +19,8 @@ const RouteBrowsers = (props) => { h(Fragment, {}, widgetBundles.map( - ({ domain, loader }) => { - const widget = props.widgets.value[loader.id] + ({ domain, widgetId }) => { + const widget = props.widgets.value[widgetId] if (widget == null) return h('p', { key: domain.id }, 'empty') diff --git a/src/ui/scripts/components/routes/RouteDevices.js b/src/ui/scripts/components/routes/RouteDevices.js index ef011adb..92388bca 100644 --- a/src/ui/scripts/components/routes/RouteDevices.js +++ b/src/ui/scripts/components/routes/RouteDevices.js @@ -19,8 +19,8 @@ const RouteDevices = (props) => { h(Fragment, {}, widgetBundles.map( - ({ domain, loader }) => { - const widget = props.widgets.value[loader.id] + ({ domain, widgetId }) => { + const widget = props.widgets.value[widgetId] if (widget == null) return h('p', { key: domain.id }, 'empty') diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index a0c169ae..29a1995f 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -18,8 +18,8 @@ const RouteLanguages = (props) => { h(Fragment, {}, widgetBundles.map( - ({ domain, loader }) => { - const widget = props.widgets.value[loader.id] + ({ domain, widgetId }) => { + const widget = props.widgets.value[widgetId] if (widget == null) return h('p', { key: domain.id }, 'empty') diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index 69ebd84f..ab464822 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -18,8 +18,8 @@ const RoutePages = (props) => { h(Fragment, {}, widgetBundles.map( - ({ domain, loader }) => { - const widget = props.widgets.value[loader.id] + ({ domain, widgetId }) => { + const widget = props.widgets.value[widgetId] if (widget == null) return h('p', { key: domain.id }, 'empty') diff --git a/src/ui/scripts/components/routes/RouteReferrers.js b/src/ui/scripts/components/routes/RouteReferrers.js index 80b9e0a8..e6ab27a4 100644 --- a/src/ui/scripts/components/routes/RouteReferrers.js +++ b/src/ui/scripts/components/routes/RouteReferrers.js @@ -18,8 +18,8 @@ const RouteReferrers = (props) => { h(Fragment, {}, widgetBundles.map( - ({ domain, loader }) => { - const widget = props.widgets.value[loader.id] + ({ domain, widgetId }) => { + const widget = props.widgets.value[widgetId] if (widget == null) return h('p', { key: domain.id }, 'empty') diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 59da740b..3d8c7564 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -31,8 +31,8 @@ const RouteViews = (props) => { // }), widgetBundles.map( - ({ domain, loader }) => { - const widget = props.widgets.value[loader.id] + ({ domain, widgetId }) => { + const widget = props.widgets.value[widgetId] if (widget == null) return h('p', { key: domain.id }, 'empty') diff --git a/src/ui/scripts/utils/useWidgetBundles.js b/src/ui/scripts/utils/useWidgetBundles.js index e0a3c6dc..cd3ae4c0 100644 --- a/src/ui/scripts/utils/useWidgetBundles.js +++ b/src/ui/scripts/utils/useWidgetBundles.js @@ -13,7 +13,7 @@ export default (props, createLoader, opts) => { return { domain, - loader + widgetId: loader.id } } ) From d4742673e7e8b2b8f9709d3ad84f3c8fd02fb6a1 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 18:31:23 +0100 Subject: [PATCH 039/208] Only pass widgetIds around --- src/ui/scripts/components/routes/RouteBrowsers.js | 12 +++++++----- src/ui/scripts/components/routes/RouteDevices.js | 12 +++++++----- src/ui/scripts/components/routes/RouteLanguages.js | 12 +++++++----- src/ui/scripts/components/routes/RoutePages.js | 12 +++++++----- src/ui/scripts/components/routes/RouteReferrers.js | 12 +++++++----- src/ui/scripts/components/routes/RouteViews.js | 12 +++++++----- .../utils/{useWidgetBundles.js => useWidgetIds.js} | 13 +++++-------- 7 files changed, 47 insertions(+), 38 deletions(-) rename src/ui/scripts/utils/{useWidgetBundles.js => useWidgetIds.js} (56%) diff --git a/src/ui/scripts/components/routes/RouteBrowsers.js b/src/ui/scripts/components/routes/RouteBrowsers.js index 3d21f7a9..0aca4804 100644 --- a/src/ui/scripts/components/routes/RouteBrowsers.js +++ b/src/ui/scripts/components/routes/RouteBrowsers.js @@ -2,14 +2,15 @@ import { createElement as h, Fragment } from 'react' import browsersLoader from '../../loaders/browsersLoader' import enhanceBrowsers from '../../enhancers/enhanceBrowsers' +import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetBundles from '../../utils/useWidgetBundles' +import useWidgetIds from '../../utils/useWidgetIds' import CardBrowsers from '../cards/CardBrowsers' const RouteBrowsers = (props) => { - const widgetBundles = useWidgetBundles(props, browsersLoader, { + const widgetIds = useWidgetIds(props, browsersLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.browsersType @@ -18,11 +19,12 @@ const RouteBrowsers = (props) => { return ( h(Fragment, {}, - widgetBundles.map( - ({ domain, widgetId }) => { + widgetIds.map( + (widgetId) => { const widget = props.widgets.value[widgetId] + if (widget == null) return h('p', {}, 'empty') - if (widget == null) return h('p', { key: domain.id }, 'empty') + const domain = selectDomainsValue.byId(props, widget.variables.domainId) return h(CardBrowsers, { key: domain.id, diff --git a/src/ui/scripts/components/routes/RouteDevices.js b/src/ui/scripts/components/routes/RouteDevices.js index 92388bca..82d6f285 100644 --- a/src/ui/scripts/components/routes/RouteDevices.js +++ b/src/ui/scripts/components/routes/RouteDevices.js @@ -2,14 +2,15 @@ import { createElement as h, Fragment } from 'react' import devicesLoader from '../../loaders/devicesLoader' import enhanceDevices from '../../enhancers/enhanceDevices' +import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetBundles from '../../utils/useWidgetBundles' +import useWidgetIds from '../../utils/useWidgetIds' import CardDevices from '../cards/CardDevices' const RouteDevices = (props) => { - const widgetBundles = useWidgetBundles(props, devicesLoader, { + const widgetIds = useWidgetIds(props, devicesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.devicesType @@ -18,11 +19,12 @@ const RouteDevices = (props) => { return ( h(Fragment, {}, - widgetBundles.map( - ({ domain, widgetId }) => { + widgetIds.map( + (widgetId) => { const widget = props.widgets.value[widgetId] + if (widget == null) return h('p', {}, 'empty') - if (widget == null) return h('p', { key: domain.id }, 'empty') + const domain = selectDomainsValue.byId(props, widget.variables.domainId) return h(CardDevices, { key: domain.id, diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index 29a1995f..f593b4b9 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -2,14 +2,15 @@ import { createElement as h, Fragment } from 'react' import languagesLoader from '../../loaders/languagesLoader' import enhanceLanguages from '../../enhancers/enhanceLanguages' +import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetBundles from '../../utils/useWidgetBundles' +import useWidgetIds from '../../utils/useWidgetIds' import CardLanguages from '../cards/CardLanguages' const RouteLanguages = (props) => { - const widgetBundles = useWidgetBundles(props, languagesLoader, { + const widgetIds = useWidgetIds(props, languagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) @@ -17,11 +18,12 @@ const RouteLanguages = (props) => { return ( h(Fragment, {}, - widgetBundles.map( - ({ domain, widgetId }) => { + widgetIds.map( + (widgetId) => { const widget = props.widgets.value[widgetId] + if (widget == null) return h('p', {}, 'empty') - if (widget == null) return h('p', { key: domain.id }, 'empty') + const domain = selectDomainsValue.byId(props, widget.variables.domainId) return h(CardLanguages, { key: domain.id, diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index ab464822..eac899be 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -2,14 +2,15 @@ import { createElement as h, Fragment } from 'react' import enhancePages from '../../enhancers/enhancePages' import pagesLoader from '../../loaders/pagesLoader' +import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetBundles from '../../utils/useWidgetBundles' +import useWidgetIds from '../../utils/useWidgetIds' import CardPages from '../cards/CardPages' const RoutePages = (props) => { - const widgetBundles = useWidgetBundles(props, pagesLoader, { + const widgetIds = useWidgetIds(props, pagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) @@ -17,11 +18,12 @@ const RoutePages = (props) => { return ( h(Fragment, {}, - widgetBundles.map( - ({ domain, widgetId }) => { + widgetIds.map( + (widgetId) => { const widget = props.widgets.value[widgetId] + if (widget == null) return h('p', {}, 'empty') - if (widget == null) return h('p', { key: domain.id }, 'empty') + const domain = selectDomainsValue.byId(props, widget.variables.domainId) return h(CardPages, { key: domain.id, diff --git a/src/ui/scripts/components/routes/RouteReferrers.js b/src/ui/scripts/components/routes/RouteReferrers.js index e6ab27a4..99155969 100644 --- a/src/ui/scripts/components/routes/RouteReferrers.js +++ b/src/ui/scripts/components/routes/RouteReferrers.js @@ -2,14 +2,15 @@ import { createElement as h, Fragment } from 'react' import referrersLoader from '../../loaders/referrersLoader' import enhanceReferrers from '../../enhancers/enhanceReferrers' +import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetBundles from '../../utils/useWidgetBundles' +import useWidgetIds from '../../utils/useWidgetIds' import CardReferrers from '../cards/CardReferrers' const RouteReferrers = (props) => { - const widgetBundles = useWidgetBundles(props, referrersLoader, { + const widgetIds = useWidgetIds(props, referrersLoader, { range: props.filter.range, sorting: props.filter.sorting }) @@ -17,11 +18,12 @@ const RouteReferrers = (props) => { return ( h(Fragment, {}, - widgetBundles.map( - ({ domain, widgetId }) => { + widgetIds.map( + (widgetId) => { const widget = props.widgets.value[widgetId] + if (widget == null) return h('p', {}, 'empty') - if (widget == null) return h('p', { key: domain.id }, 'empty') + const domain = selectDomainsValue.byId(props, widget.variables.domainId) return h(CardReferrers, { key: domain.id, diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 3d8c7564..74926ea2 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -3,15 +3,16 @@ import { createElement as h, Fragment } from 'react' // import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' import viewsLoader from '../../loaders/viewsLoader' import enhanceViews from '../../enhancers/enhanceViews' +import * as selectDomainsValue from '../../selectors/selectDomainsValue' // import mergeViews from '../../utils/mergeViews' import overviewRoute from '../../utils/overviewRoute' -import useWidgetBundles from '../../utils/useWidgetBundles' +import useWidgetIds from '../../utils/useWidgetIds' import CardViews from '../cards/CardViews' const RouteViews = (props) => { - const widgetBundles = useWidgetBundles(props, viewsLoader, { + const widgetIds = useWidgetIds(props, viewsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) @@ -30,11 +31,12 @@ const RouteViews = (props) => { // items: mergeViews(props) // }), - widgetBundles.map( - ({ domain, widgetId }) => { + widgetIds.map( + (widgetId) => { const widget = props.widgets.value[widgetId] + if (widget == null) return h('p', {}, 'empty') - if (widget == null) return h('p', { key: domain.id }, 'empty') + const domain = selectDomainsValue.byId(props, widget.variables.domainId) return h(CardViews, { key: domain.id, diff --git a/src/ui/scripts/utils/useWidgetBundles.js b/src/ui/scripts/utils/useWidgetIds.js similarity index 56% rename from src/ui/scripts/utils/useWidgetBundles.js rename to src/ui/scripts/utils/useWidgetIds.js index cd3ae4c0..c9a847a1 100644 --- a/src/ui/scripts/utils/useWidgetBundles.js +++ b/src/ui/scripts/utils/useWidgetIds.js @@ -2,26 +2,23 @@ import { useEffect, useState } from 'react' export default (props, createLoader, opts) => { - const [ widgetBundles, setWidgetBundles ] = useState([]) + const [ widgetIds, setWidgetIds ] = useState([]) useEffect(() => { - const widgetBundles = props.domains.value.map( + const widgetIds = props.domains.value.map( (domain) => { const loader = createLoader(domain.id, opts) props.fetchWidget(props, loader) - return { - domain, - widgetId: loader.id - } + return loader.id } ) - setWidgetBundles(widgetBundles) + setWidgetIds(widgetIds) }, [ props.domains.value, ...Object.values(opts) ]) - return widgetBundles + return widgetIds } \ No newline at end of file From 88205d8f603acc12a1979721c03cd13c0eb9293d Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 20:01:35 +0100 Subject: [PATCH 040/208] Use widget loading for sizes --- src/ui/scripts/actions/filter.js | 6 ++ src/ui/scripts/actions/index.js | 1 - src/ui/scripts/actions/sizes.js | 75 ------------------- src/ui/scripts/components/Filter.js | 14 ++-- .../scripts/components/routes/RouteSizes.js | 37 +++++---- src/ui/scripts/enhancers/enhanceState.js | 2 - src/ui/scripts/index.js | 5 -- src/ui/scripts/loaders/sizesLoader.js | 37 +++++++++ src/ui/scripts/reducers/filter.js | 10 ++- src/ui/scripts/reducers/index.js | 2 - src/ui/scripts/reducers/sizes.js | 46 ------------ src/ui/scripts/selectors/selectSizesValue.js | 6 -- 12 files changed, 80 insertions(+), 161 deletions(-) delete mode 100644 src/ui/scripts/actions/sizes.js create mode 100644 src/ui/scripts/loaders/sizesLoader.js delete mode 100644 src/ui/scripts/reducers/sizes.js delete mode 100644 src/ui/scripts/selectors/selectSizesValue.js diff --git a/src/ui/scripts/actions/filter.js b/src/ui/scripts/actions/filter.js index 673887b1..bd420128 100644 --- a/src/ui/scripts/actions/filter.js +++ b/src/ui/scripts/actions/filter.js @@ -4,6 +4,7 @@ export const SET_FILTER_INTERVAL = Symbol() export const SET_FILTER_VIEWS_TYPE = Symbol() export const SET_FILTER_DEVICES_TYPE = Symbol() export const SET_FILTER_BROWSERS_TYPE = Symbol() +export const SET_FILTER_SIZES_TYPE = Symbol() export const setFilterSorting = (payload) => ({ type: SET_FILTER_SORTING, @@ -33,4 +34,9 @@ export const setFilterDevicesType = (payload) => ({ export const setFilterBrowsersType = (payload) => ({ type: SET_FILTER_BROWSERS_TYPE, payload +}) + +export const setFilterSizesType = (payload) => ({ + type: SET_FILTER_SIZES_TYPE, + payload }) \ No newline at end of file diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index db2a7836..edfee99e 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -7,7 +7,6 @@ export * from './domains' export * from './events' export * from './overview' export * from './durations' -export * from './sizes' export * from './systems' export * from './widgets' diff --git a/src/ui/scripts/actions/sizes.js b/src/ui/scripts/actions/sizes.js deleted file mode 100644 index 410da14a..00000000 --- a/src/ui/scripts/actions/sizes.js +++ /dev/null @@ -1,75 +0,0 @@ -import api from '../utils/api' -import signalHandler from '../utils/signalHandler' - -export const SET_SIZES_TYPE = Symbol() -export const SET_SIZES_VALUE = Symbol() -export const SET_SIZES_FETCHING = Symbol() -export const SET_SIZES_ERROR = Symbol() - -export const setSizesType = (payload) => ({ - type: SET_SIZES_TYPE, - payload -}) - -export const setSizesValue = (domainId, payload) => ({ - type: SET_SIZES_VALUE, - domainId, - payload -}) - -export const setSizesFetching = (payload) => ({ - type: SET_SIZES_FETCHING, - payload -}) - -export const setSizesError = (payload) => ({ - type: SET_SIZES_ERROR, - payload -}) - -export const fetchSizes = signalHandler((signal) => (props) => async (dispatch) => { - - dispatch(setSizesFetching(true)) - dispatch(setSizesError()) - - try { - - const data = await api({ - query: ` - query fetchSizes($sorting: Sorting!, $type: SizeType!, $range: Range) { - domains { - id - statistics { - sizes(sorting: $sorting, type: $type, range: $range) { - id - count - created - } - } - } - } - `, - variables: { - sorting: props.filter.sorting, - type: props.sizes.type, - range: props.filter.range - }, - props, - signal: signal() - }) - - data.domains.forEach((domain) => { - dispatch(setSizesValue(domain.id, domain.statistics.sizes)) - }) - dispatch(setSizesFetching(false)) - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setSizesFetching(false)) - if (err.name === 'HandledError') return - dispatch(setSizesError(err)) - - } - -}) \ No newline at end of file diff --git a/src/ui/scripts/components/Filter.js b/src/ui/scripts/components/Filter.js index 7506530b..906b25f3 100644 --- a/src/ui/scripts/components/Filter.js +++ b/src/ui/scripts/components/Filter.js @@ -213,14 +213,14 @@ const Filter = (props) => { ], [ROUTE_SIZES.key]: [ sortingItem, - createItem(labels.sizes[props.sizes.type], [ - createButton('Browser sizes', 'Width and height combined', props.setSizesType, props.sizes.type, sizes.SIZES_TYPE_BROWSER_RESOLUTION), - createButton('↳ widths', undefined, props.setSizesType, props.sizes.type, sizes.SIZES_TYPE_BROWSER_WIDTH), - createButton('↳ heights', undefined, props.setSizesType, props.sizes.type, sizes.SIZES_TYPE_BROWSER_HEIGHT), + createItem(labels.sizes[props.filter.sizesType], [ + createButton('Browser sizes', 'Width and height combined', props.setFilterSizesType, props.filter.sizesType, sizes.SIZES_TYPE_BROWSER_RESOLUTION), + createButton('↳ widths', undefined, props.setFilterSizesType, props.filter.sizesType, sizes.SIZES_TYPE_BROWSER_WIDTH), + createButton('↳ heights', undefined, props.setFilterSizesType, props.filter.sizesType, sizes.SIZES_TYPE_BROWSER_HEIGHT), createSeparator(), - createButton('Screen sizes', 'Width and height combined', props.setSizesType, props.sizes.type, sizes.SIZES_TYPE_SCREEN_RESOLUTION), - createButton('↳ widths', undefined, props.setSizesType, props.sizes.type, sizes.SIZES_TYPE_SCREEN_WIDTH), - createButton('↳ heights', undefined, props.setSizesType, props.sizes.type, sizes.SIZES_TYPE_SCREEN_HEIGHT) + createButton('Screen sizes', 'Width and height combined', props.setFilterSizesType, props.filter.sizesType, sizes.SIZES_TYPE_SCREEN_RESOLUTION), + createButton('↳ widths', undefined, props.setFilterSizesType, props.filter.sizesType, sizes.SIZES_TYPE_SCREEN_WIDTH), + createButton('↳ heights', undefined, props.setFilterSizesType, props.filter.sizesType, sizes.SIZES_TYPE_SCREEN_HEIGHT) ]), rangeItem ], diff --git a/src/ui/scripts/components/routes/RouteSizes.js b/src/ui/scripts/components/routes/RouteSizes.js index 2e29732e..62056156 100644 --- a/src/ui/scripts/components/routes/RouteSizes.js +++ b/src/ui/scripts/components/routes/RouteSizes.js @@ -1,34 +1,41 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h, Fragment } from 'react' -import selectSizesValue from '../../selectors/selectSizesValue' +import sizesLoader from '../../loaders/sizesLoader' import enhanceSizes from '../../enhancers/enhanceSizes' +import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' +import useWidgetIds from '../../utils/useWidgetIds' import CardSizes from '../cards/CardSizes' const RouteSizes = (props) => { - useEffect(() => { - - props.fetchSizes(props) - - }, [ props.filter.range, props.filter.sorting, props.sizes.type ]) + const widgetIds = useWidgetIds(props, sizesLoader, { + range: props.filter.range, + sorting: props.filter.sorting, + type: props.filter.sizesType + }) return ( h(Fragment, {}, - props.domains.value.map( - (domain) => ( - h(CardSizes, { + widgetIds.map( + (widgetId) => { + const widget = props.widgets.value[widgetId] + if (widget == null) return h('p', {}, 'empty') + + const domain = selectDomainsValue.byId(props, widget.variables.domainId) + + return h(CardSizes, { key: domain.id, headline: domain.title, - range: props.filter.range, - sorting: props.filter.sorting, - loading: props.sizes.fetching, - items: enhanceSizes(selectSizesValue(props, domain.id).value), + range: widget.variables.range, + sorting: widget.variables.sorting, + loading: widget.fetching, + items: enhanceSizes(widget.value), onMore: () => props.setRoute(overviewRoute(domain)) }) - ) + } ) ) diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index 5875fef0..5ec0cadd 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -10,7 +10,6 @@ export default (state) => { Object.values(state.widgets.value).some((value) => value.fetching) === true || state.durations.fetching === true || state.systems.fetching === true || - state.sizes.fetching === true || state.domains.fetching === true || state.token.fetching === true || state.permanentTokens.fetching === true || @@ -23,7 +22,6 @@ export default (state) => { ...Object.values(state.widgets.value).map((value) => value.error), state.durations.error, state.systems.error, - state.sizes.error, state.domains.error, state.token.error, state.permanentTokens.error, diff --git a/src/ui/scripts/index.js b/src/ui/scripts/index.js index 8515fccc..d83f95b4 100644 --- a/src/ui/scripts/index.js +++ b/src/ui/scripts/index.js @@ -15,7 +15,6 @@ import { initialState as initialTokenState } from './reducers/token' import { initialState as initialRouteState } from './reducers/route' import { initialState as initialFilterState } from './reducers/filter' import { initialState as initialSystemsState } from './reducers/systems' -import { initialState as initialSizesState } from './reducers/sizes' import Main from './components/Main' @@ -58,10 +57,6 @@ store.subscribe(() => { systems: { ...initialSystemsState(), type: currentState.systems.type - }, - sizes: { - ...initialSizesState(), - type: currentState.sizes.type } }) diff --git a/src/ui/scripts/loaders/sizesLoader.js b/src/ui/scripts/loaders/sizesLoader.js new file mode 100644 index 00000000..c93f21ef --- /dev/null +++ b/src/ui/scripts/loaders/sizesLoader.js @@ -0,0 +1,37 @@ +export default (domainId, opts) => { + + // TODO: Improve ids + const id = `fetchSizes${ domainId }${ JSON.stringify(opts) }` + + const query = ` + query fetchSizes($domainId: ID!, $sorting: Sorting!, $type: SizeType!, $range: Range) { + domain(id: $domainId) { + id + statistics { + sizes(sorting: $sorting, type: $type, range: $range) { + id + count + created + } + } + } + } + ` + + const variables = { + domainId, + sorting: opts.sorting, + range: opts.range, + type: opts.type + } + + const selector = (data) => data.domain.statistics.sizes + + return { + id, + query, + variables, + selector + } + +} \ No newline at end of file diff --git a/src/ui/scripts/reducers/filter.js b/src/ui/scripts/reducers/filter.js index 18bd1259..fbdce26a 100644 --- a/src/ui/scripts/reducers/filter.js +++ b/src/ui/scripts/reducers/filter.js @@ -6,7 +6,8 @@ import { SET_FILTER_INTERVAL, SET_FILTER_VIEWS_TYPE, SET_FILTER_DEVICES_TYPE, - SET_FILTER_BROWSERS_TYPE + SET_FILTER_BROWSERS_TYPE, + SET_FILTER_SIZES_TYPE } from '../actions' import { SORTINGS_TOP } from '../../../constants/sortings' @@ -15,6 +16,7 @@ import { INTERVALS_DAILY } from '../../../constants/intervals' import { VIEWS_TYPE_UNIQUE } from '../../../constants/views' import { DEVICES_TYPE_WITH_MODEL } from '../../../constants/devices' import { BROWSERS_TYPE_WITH_VERSION } from '../../../constants/browsers' +import { SIZES_TYPE_BROWSER_RESOLUTION } from '../../../constants/sizes' export const initialState = () => ({ sorting: SORTINGS_TOP, @@ -22,7 +24,8 @@ export const initialState = () => ({ interval: INTERVALS_DAILY, viewsType: VIEWS_TYPE_UNIQUE, devicesType: DEVICES_TYPE_WITH_MODEL, - browsersType: BROWSERS_TYPE_WITH_VERSION + browsersType: BROWSERS_TYPE_WITH_VERSION, + sizesType: SIZES_TYPE_BROWSER_RESOLUTION }) export default produce((draft, action) => { @@ -46,6 +49,9 @@ export default produce((draft, action) => { case SET_FILTER_BROWSERS_TYPE: draft.browsersType = action.payload break + case SET_FILTER_SIZES_TYPE: + draft.sizesType = action.payload + break } }, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/index.js b/src/ui/scripts/reducers/index.js index 47d6e9aa..8e1534d7 100644 --- a/src/ui/scripts/reducers/index.js +++ b/src/ui/scripts/reducers/index.js @@ -14,7 +14,6 @@ import events from './events' import overview from './overview' import durations from './durations' import systems from './systems' -import sizes from './sizes' import widgets from './widgets' const reducers = combineReducers({ @@ -28,7 +27,6 @@ const reducers = combineReducers({ overview, durations, systems, - sizes, widgets }) diff --git a/src/ui/scripts/reducers/sizes.js b/src/ui/scripts/reducers/sizes.js deleted file mode 100644 index e9170def..00000000 --- a/src/ui/scripts/reducers/sizes.js +++ /dev/null @@ -1,46 +0,0 @@ -import produce from 'immer' - -import { - SET_SIZES_TYPE, - SET_SIZES_VALUE, - SET_SIZES_FETCHING, - SET_SIZES_ERROR -} from '../actions' - -import { SIZES_TYPE_BROWSER_RESOLUTION } from '../../../constants/sizes' -import genericState from '../utils/genericState' -import genericSubState from '../utils/genericSubState' - -export const initialState = () => ({ - type: SIZES_TYPE_BROWSER_RESOLUTION, - ...genericState() -}) - -export const initialSubState = genericSubState - -export default produce((draft, action) => { - - const hasDomainId = () => action.domainId != null - const hasDomainValue = () => draft.value[action.domainId] != null - - if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() - - switch (action.type) { - case SET_SIZES_TYPE: - if (draft.type === action.payload) break - // Reset value because the view shouldn't show the old data when switching - draft.value = initialState().value - draft.type = action.payload - break - case SET_SIZES_VALUE: - draft.value[action.domainId].value = action.payload || initialSubState().value - break - case SET_SIZES_FETCHING: - draft.fetching = action.payload || initialState().fetching - break - case SET_SIZES_ERROR: - draft.error = action.payload || initialState().error - break - } - -}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/selectors/selectSizesValue.js b/src/ui/scripts/selectors/selectSizesValue.js deleted file mode 100644 index 0c340704..00000000 --- a/src/ui/scripts/selectors/selectSizesValue.js +++ /dev/null @@ -1,6 +0,0 @@ -import { initialSubState } from '../reducers/sizes' - -export default (state, domainId) => { - const value = state.sizes.value[domainId] - return value == null ? initialSubState() : value -} \ No newline at end of file From b5b7773155aefcc5a96c4e5ea30c4e7fc114605c Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 22 Nov 2020 20:18:49 +0100 Subject: [PATCH 041/208] Load systems via widget loader --- src/ui/scripts/actions/filter.js | 6 ++ src/ui/scripts/actions/index.js | 1 - src/ui/scripts/actions/systems.js | 75 ------------------- src/ui/scripts/components/Filter.js | 4 +- .../scripts/components/routes/RouteSystems.js | 37 +++++---- src/ui/scripts/enhancers/enhanceState.js | 2 - src/ui/scripts/index.js | 5 -- src/ui/scripts/loaders/systemsLoader.js | 37 +++++++++ src/ui/scripts/reducers/filter.js | 10 ++- src/ui/scripts/reducers/index.js | 2 - src/ui/scripts/reducers/systems.js | 46 ------------ .../scripts/selectors/selectSystemsValue.js | 6 -- 12 files changed, 75 insertions(+), 156 deletions(-) delete mode 100644 src/ui/scripts/actions/systems.js create mode 100644 src/ui/scripts/loaders/systemsLoader.js delete mode 100644 src/ui/scripts/reducers/systems.js delete mode 100644 src/ui/scripts/selectors/selectSystemsValue.js diff --git a/src/ui/scripts/actions/filter.js b/src/ui/scripts/actions/filter.js index bd420128..bb5563fa 100644 --- a/src/ui/scripts/actions/filter.js +++ b/src/ui/scripts/actions/filter.js @@ -5,6 +5,7 @@ export const SET_FILTER_VIEWS_TYPE = Symbol() export const SET_FILTER_DEVICES_TYPE = Symbol() export const SET_FILTER_BROWSERS_TYPE = Symbol() export const SET_FILTER_SIZES_TYPE = Symbol() +export const SET_FILTER_SYSTEMS_TYPE = Symbol() export const setFilterSorting = (payload) => ({ type: SET_FILTER_SORTING, @@ -39,4 +40,9 @@ export const setFilterBrowsersType = (payload) => ({ export const setFilterSizesType = (payload) => ({ type: SET_FILTER_SIZES_TYPE, payload +}) + +export const setFilterSystemsType = (payload) => ({ + type: SET_FILTER_SYSTEMS_TYPE, + payload }) \ No newline at end of file diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index edfee99e..5fb418a3 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -7,7 +7,6 @@ export * from './domains' export * from './events' export * from './overview' export * from './durations' -export * from './systems' export * from './widgets' export const RESET_STATE = Symbol() diff --git a/src/ui/scripts/actions/systems.js b/src/ui/scripts/actions/systems.js deleted file mode 100644 index e2a1feb2..00000000 --- a/src/ui/scripts/actions/systems.js +++ /dev/null @@ -1,75 +0,0 @@ -import api from '../utils/api' -import signalHandler from '../utils/signalHandler' - -export const SET_SYSTEMS_TYPE = Symbol() -export const SET_SYSTEMS_VALUE = Symbol() -export const SET_SYSTEMS_FETCHING = Symbol() -export const SET_SYSTEMS_ERROR = Symbol() - -export const setSystemsType = (payload) => ({ - type: SET_SYSTEMS_TYPE, - payload -}) - -export const setSystemsValue = (domainId, payload) => ({ - type: SET_SYSTEMS_VALUE, - domainId, - payload -}) - -export const setSystemsFetching = (payload) => ({ - type: SET_SYSTEMS_FETCHING, - payload -}) - -export const setSystemsError = (payload) => ({ - type: SET_SYSTEMS_ERROR, - payload -}) - -export const fetchSystems = signalHandler((signal) => (props) => async (dispatch) => { - - dispatch(setSystemsFetching(true)) - dispatch(setSystemsError()) - - try { - - const data = await api({ - query: ` - query fetchSystems($sorting: Sorting!, $type: SystemType!, $range: Range) { - domains { - id - statistics { - systems(sorting: $sorting, type: $type, range: $range) { - id - count - created - } - } - } - } - `, - variables: { - sorting: props.filter.sorting, - type: props.systems.type, - range: props.filter.range - }, - props, - signal: signal() - }) - - data.domains.forEach((domain) => { - dispatch(setSystemsValue(domain.id, domain.statistics.systems)) - }) - dispatch(setSystemsFetching(false)) - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setSystemsFetching(false)) - if (err.name === 'HandledError') return - dispatch(setSystemsError(err)) - - } - -}) \ No newline at end of file diff --git a/src/ui/scripts/components/Filter.js b/src/ui/scripts/components/Filter.js index 906b25f3..1bdbc37a 100644 --- a/src/ui/scripts/components/Filter.js +++ b/src/ui/scripts/components/Filter.js @@ -183,8 +183,8 @@ const Filter = (props) => { ...sortingButtons, createSeparator(), onlyInactiveButton( - createButton('Show version', 'Include system version', props.setSystemsType, props.systems.type, systems.SYSTEMS_TYPE_WITH_VERSION), - createButton('Hide version', 'Don\'t include version', props.setSystemsType, props.systems.type, systems.SYSTEMS_TYPE_NO_VERSION) + createButton('Show version', 'Include system version', props.setFilterSystemsType, props.filter.systemsType, systems.SYSTEMS_TYPE_WITH_VERSION), + createButton('Hide version', 'Don\'t include version', props.setFilterSystemsType, props.filter.systemsType, systems.SYSTEMS_TYPE_NO_VERSION) ) ]), rangeItem diff --git a/src/ui/scripts/components/routes/RouteSystems.js b/src/ui/scripts/components/routes/RouteSystems.js index 201b3934..aebcc68c 100644 --- a/src/ui/scripts/components/routes/RouteSystems.js +++ b/src/ui/scripts/components/routes/RouteSystems.js @@ -1,34 +1,41 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h, Fragment } from 'react' -import selectSystemsValue from '../../selectors/selectSystemsValue' +import systemsLoader from '../../loaders/systemsLoader' import enhanceSystems from '../../enhancers/enhanceSystems' +import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' +import useWidgetIds from '../../utils/useWidgetIds' import CardSystems from '../cards/CardSystems' const RouteSystems = (props) => { - useEffect(() => { - - props.fetchSystems(props) - - }, [ props.filter.range, props.filter.sorting, props.systems.type ]) + const widgetIds = useWidgetIds(props, systemsLoader, { + range: props.filter.range, + sorting: props.filter.sorting, + type: props.filter.systemsType + }) return ( h(Fragment, {}, - props.domains.value.map( - (domain) => ( - h(CardSystems, { + widgetIds.map( + (widgetId) => { + const widget = props.widgets.value[widgetId] + if (widget == null) return h('p', {}, 'empty') + + const domain = selectDomainsValue.byId(props, widget.variables.domainId) + + return h(CardSystems, { key: domain.id, headline: domain.title, - range: props.filter.range, - sorting: props.filter.sorting, - loading: props.systems.fetching, - items: enhanceSystems(selectSystemsValue(props, domain.id).value), + range: widget.variables.range, + sorting: widget.variables.sorting, + loading: widget.fetching, + items: enhanceSystems(widget.value), onMore: () => props.setRoute(overviewRoute(domain)) }) - ) + } ) ) diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index 5ec0cadd..a751b204 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -9,7 +9,6 @@ export default (state) => { Object.values(state.overview.value).some((value) => value.fetching) === true || Object.values(state.widgets.value).some((value) => value.fetching) === true || state.durations.fetching === true || - state.systems.fetching === true || state.domains.fetching === true || state.token.fetching === true || state.permanentTokens.fetching === true || @@ -21,7 +20,6 @@ export default (state) => { ...Object.values(state.overview.value).map((value) => value.error), ...Object.values(state.widgets.value).map((value) => value.error), state.durations.error, - state.systems.error, state.domains.error, state.token.error, state.permanentTokens.error, diff --git a/src/ui/scripts/index.js b/src/ui/scripts/index.js index d83f95b4..9904a6c8 100644 --- a/src/ui/scripts/index.js +++ b/src/ui/scripts/index.js @@ -14,7 +14,6 @@ import * as actions from './actions' import { initialState as initialTokenState } from './reducers/token' import { initialState as initialRouteState } from './reducers/route' import { initialState as initialFilterState } from './reducers/filter' -import { initialState as initialSystemsState } from './reducers/systems' import Main from './components/Main' @@ -53,10 +52,6 @@ store.subscribe(() => { viewsType: currentState.filter.viewsType, devicesType: currentState.filter.devicesType, browsersType: currentState.filter.browsersType - }, - systems: { - ...initialSystemsState(), - type: currentState.systems.type } }) diff --git a/src/ui/scripts/loaders/systemsLoader.js b/src/ui/scripts/loaders/systemsLoader.js new file mode 100644 index 00000000..19d5506b --- /dev/null +++ b/src/ui/scripts/loaders/systemsLoader.js @@ -0,0 +1,37 @@ +export default (domainId, opts) => { + + // TODO: Improve ids + const id = `fetchSystems${ domainId }${ JSON.stringify(opts) }` + + const query = ` + query fetchSystems($domainId: ID!, $sorting: Sorting!, $type: SystemType!, $range: Range) { + domain(id: $domainId) { + id + statistics { + systems(sorting: $sorting, type: $type, range: $range) { + id + count + created + } + } + } + } + ` + + const variables = { + domainId, + sorting: opts.sorting, + range: opts.range, + type: opts.type + } + + const selector = (data) => data.domain.statistics.systems + + return { + id, + query, + variables, + selector + } + +} \ No newline at end of file diff --git a/src/ui/scripts/reducers/filter.js b/src/ui/scripts/reducers/filter.js index fbdce26a..d96ea6b5 100644 --- a/src/ui/scripts/reducers/filter.js +++ b/src/ui/scripts/reducers/filter.js @@ -7,7 +7,8 @@ import { SET_FILTER_VIEWS_TYPE, SET_FILTER_DEVICES_TYPE, SET_FILTER_BROWSERS_TYPE, - SET_FILTER_SIZES_TYPE + SET_FILTER_SIZES_TYPE, + SET_FILTER_SYSTEMS_TYPE } from '../actions' import { SORTINGS_TOP } from '../../../constants/sortings' @@ -17,6 +18,7 @@ import { VIEWS_TYPE_UNIQUE } from '../../../constants/views' import { DEVICES_TYPE_WITH_MODEL } from '../../../constants/devices' import { BROWSERS_TYPE_WITH_VERSION } from '../../../constants/browsers' import { SIZES_TYPE_BROWSER_RESOLUTION } from '../../../constants/sizes' +import { SYSTEMS_TYPE_WITH_VERSION } from '../../../constants/systems' export const initialState = () => ({ sorting: SORTINGS_TOP, @@ -25,7 +27,8 @@ export const initialState = () => ({ viewsType: VIEWS_TYPE_UNIQUE, devicesType: DEVICES_TYPE_WITH_MODEL, browsersType: BROWSERS_TYPE_WITH_VERSION, - sizesType: SIZES_TYPE_BROWSER_RESOLUTION + sizesType: SIZES_TYPE_BROWSER_RESOLUTION, + systemsType: SYSTEMS_TYPE_WITH_VERSION }) export default produce((draft, action) => { @@ -52,6 +55,9 @@ export default produce((draft, action) => { case SET_FILTER_SIZES_TYPE: draft.sizesType = action.payload break + case SET_FILTER_SYSTEMS_TYPE: + draft.systemsType = action.payload + break } }, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/index.js b/src/ui/scripts/reducers/index.js index 8e1534d7..9c65e522 100644 --- a/src/ui/scripts/reducers/index.js +++ b/src/ui/scripts/reducers/index.js @@ -13,7 +13,6 @@ import domains from './domains' import events from './events' import overview from './overview' import durations from './durations' -import systems from './systems' import widgets from './widgets' const reducers = combineReducers({ @@ -26,7 +25,6 @@ const reducers = combineReducers({ events, overview, durations, - systems, widgets }) diff --git a/src/ui/scripts/reducers/systems.js b/src/ui/scripts/reducers/systems.js deleted file mode 100644 index 3ac376c3..00000000 --- a/src/ui/scripts/reducers/systems.js +++ /dev/null @@ -1,46 +0,0 @@ -import produce from 'immer' - -import { - SET_SYSTEMS_ERROR, - SET_SYSTEMS_FETCHING, - SET_SYSTEMS_VALUE, - SET_SYSTEMS_TYPE -} from '../actions' - -import { SYSTEMS_TYPE_NO_VERSION } from '../../../constants/systems' -import genericState from '../utils/genericState' -import genericSubState from '../utils/genericSubState' - -export const initialState = () => ({ - type: SYSTEMS_TYPE_NO_VERSION, - ...genericState() -}) - -export const initialSubState = genericSubState - -export default produce((draft, action) => { - - const hasDomainId = () => action.domainId != null - const hasDomainValue = () => draft.value[action.domainId] != null - - if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() - - switch (action.type) { - case SET_SYSTEMS_TYPE: - if (draft.type === action.payload) break - // Reset value because the view shouldn't show the old data when switching - draft.value = initialState().value - draft.type = action.payload - break - case SET_SYSTEMS_VALUE: - draft.value[action.domainId].value = action.payload || initialSubState().value - break - case SET_SYSTEMS_FETCHING: - draft.fetching = action.payload || initialState().fetching - break - case SET_SYSTEMS_ERROR: - draft.error = action.payload || initialState().error - break - } - -}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/selectors/selectSystemsValue.js b/src/ui/scripts/selectors/selectSystemsValue.js deleted file mode 100644 index fc548d9c..00000000 --- a/src/ui/scripts/selectors/selectSystemsValue.js +++ /dev/null @@ -1,6 +0,0 @@ -import { initialSubState } from '../reducers/systems' - -export default (state, domainId) => { - const value = state.systems.value[domainId] - return value == null ? initialSubState() : value -} \ No newline at end of file From a0454f53fd441e70a18a1efcbf6f7766b93755ac Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 15:05:12 +0100 Subject: [PATCH 042/208] Switch durations to widgets + useWidgets --- src/ui/scripts/actions/durations.js | 66 ------------------- src/ui/scripts/actions/index.js | 1 - .../components/routes/RouteBrowsers.js | 9 ++- .../scripts/components/routes/RouteDevices.js | 9 ++- .../components/routes/RouteDurations.js | 35 +++++----- .../components/routes/RouteLanguages.js | 9 ++- .../scripts/components/routes/RoutePages.js | 9 ++- .../components/routes/RouteReferrers.js | 9 ++- .../scripts/components/routes/RouteSizes.js | 9 ++- .../scripts/components/routes/RouteSystems.js | 9 ++- .../scripts/components/routes/RouteViews.js | 37 +++++------ src/ui/scripts/enhancers/enhanceState.js | 2 - src/ui/scripts/loaders/durationsLoader.js | 34 ++++++++++ src/ui/scripts/reducers/durations.js | 35 ---------- src/ui/scripts/reducers/index.js | 2 - .../scripts/selectors/selectDurationsValue.js | 6 -- src/ui/scripts/selectors/selectViewsValue.js | 7 -- src/ui/scripts/utils/mergeDurations.js | 14 ++-- src/ui/scripts/utils/mergeViews.js | 14 ++-- .../utils/{useWidgetIds.js => useWidgets.js} | 5 +- 20 files changed, 115 insertions(+), 206 deletions(-) delete mode 100644 src/ui/scripts/actions/durations.js create mode 100644 src/ui/scripts/loaders/durationsLoader.js delete mode 100644 src/ui/scripts/reducers/durations.js delete mode 100644 src/ui/scripts/selectors/selectDurationsValue.js delete mode 100644 src/ui/scripts/selectors/selectViewsValue.js rename src/ui/scripts/utils/{useWidgetIds.js => useWidgets.js} (77%) diff --git a/src/ui/scripts/actions/durations.js b/src/ui/scripts/actions/durations.js deleted file mode 100644 index 2332b697..00000000 --- a/src/ui/scripts/actions/durations.js +++ /dev/null @@ -1,66 +0,0 @@ -import api from '../utils/api' -import signalHandler from '../utils/signalHandler' - -export const SET_DURATIONS_VALUE = Symbol() -export const SET_DURATIONS_FETCHING = Symbol() -export const SET_DURATIONS_ERROR = Symbol() - -export const setDurationsValue = (domainId, payload) => ({ - type: SET_DURATIONS_VALUE, - domainId, - payload -}) - -export const setDurationsFetching = (payload) => ({ - type: SET_DURATIONS_FETCHING, - payload -}) - -export const setDurationsError = (payload) => ({ - type: SET_DURATIONS_ERROR, - payload -}) - -export const fetchDurations = signalHandler((signal) => (props) => async (dispatch) => { - - dispatch(setDurationsFetching(true)) - dispatch(setDurationsError()) - - try { - - const data = await api({ - query: ` - query fetchDurations($interval: Interval!) { - domains { - id - statistics { - durations(interval: $interval) { - id - count - } - } - } - } - `, - variables: { - interval: props.filter.interval - }, - props, - signal: signal() - }) - - data.domains.forEach((domain) => { - dispatch(setDurationsValue(domain.id, domain.statistics.durations)) - }) - dispatch(setDurationsFetching(false)) - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setDurationsFetching(false)) - if (err.name === 'HandledError') return - dispatch(setDurationsError(err)) - - } - -}) \ No newline at end of file diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index 5fb418a3..cebd854f 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -6,7 +6,6 @@ export * from './filter' export * from './domains' export * from './events' export * from './overview' -export * from './durations' export * from './widgets' export const RESET_STATE = Symbol() diff --git a/src/ui/scripts/components/routes/RouteBrowsers.js b/src/ui/scripts/components/routes/RouteBrowsers.js index 0aca4804..3570781d 100644 --- a/src/ui/scripts/components/routes/RouteBrowsers.js +++ b/src/ui/scripts/components/routes/RouteBrowsers.js @@ -4,13 +4,13 @@ import browsersLoader from '../../loaders/browsersLoader' import enhanceBrowsers from '../../enhancers/enhanceBrowsers' import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetIds from '../../utils/useWidgetIds' +import useWidgets from '../../utils/useWidgets' import CardBrowsers from '../cards/CardBrowsers' const RouteBrowsers = (props) => { - const widgetIds = useWidgetIds(props, browsersLoader, { + const widgets = useWidgets(props, browsersLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.browsersType @@ -19,9 +19,8 @@ const RouteBrowsers = (props) => { return ( h(Fragment, {}, - widgetIds.map( - (widgetId) => { - const widget = props.widgets.value[widgetId] + widgets.map( + (widget) => { if (widget == null) return h('p', {}, 'empty') const domain = selectDomainsValue.byId(props, widget.variables.domainId) diff --git a/src/ui/scripts/components/routes/RouteDevices.js b/src/ui/scripts/components/routes/RouteDevices.js index 82d6f285..590d2336 100644 --- a/src/ui/scripts/components/routes/RouteDevices.js +++ b/src/ui/scripts/components/routes/RouteDevices.js @@ -4,13 +4,13 @@ import devicesLoader from '../../loaders/devicesLoader' import enhanceDevices from '../../enhancers/enhanceDevices' import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetIds from '../../utils/useWidgetIds' +import useWidgets from '../../utils/useWidgets' import CardDevices from '../cards/CardDevices' const RouteDevices = (props) => { - const widgetIds = useWidgetIds(props, devicesLoader, { + const widgets = useWidgets(props, devicesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.devicesType @@ -19,9 +19,8 @@ const RouteDevices = (props) => { return ( h(Fragment, {}, - widgetIds.map( - (widgetId) => { - const widget = props.widgets.value[widgetId] + widgets.map( + (widget) => { if (widget == null) return h('p', {}, 'empty') const domain = selectDomainsValue.byId(props, widget.variables.domainId) diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index a703ab00..a9b3ee8a 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -1,19 +1,20 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h, Fragment } from 'react' -import selectDurationsValue from '../../selectors/selectDurationsValue' +import durationsLoader from '../../loaders/durationsLoader' import enhanceDurations from '../../enhancers/enhanceDurations' +import * as selectDomainsValue from '../../selectors/selectDomainsValue' import mergeDurations from '../../utils/mergeDurations' import overviewRoute from '../../utils/overviewRoute' +import useWidgets from '../../utils/useWidgets' import CardDurations from '../cards/CardDurations' const RouteDurations = (props) => { - useEffect(() => { - - props.fetchDurations(props) - - }, [ props.filter.interval ]) + const widgets = useWidgets(props, durationsLoader, { + interval: props.filter.interval, + type: props.filter.viewsType + }) return ( h(Fragment, {}, @@ -22,20 +23,24 @@ const RouteDurations = (props) => { headline: 'Durations', interval: props.filter.interval, loading: props.fetching, - items: mergeDurations(props) + items: mergeDurations(widgets) }), - props.domains.value.map( - (domain) => ( - h(CardDurations, { + widgets.map( + (widget) => { + if (widget == null) return h('p', {}, 'empty') + + const domain = selectDomainsValue.byId(props, widget.variables.domainId) + + return h(CardDurations, { key: domain.id, headline: domain.title, - interval: props.filter.interval, - loading: props.durations.fetching, - items: enhanceDurations(selectDurationsValue(props, domain.id).value, 7), + interval: widget.variables.interval, + loading: widget.fetching, + items: enhanceDurations(widget.value, 7), onMore: () => props.setRoute(overviewRoute(domain)) }) - ) + } ) ) ) diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index f593b4b9..6aae82dc 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -4,13 +4,13 @@ import languagesLoader from '../../loaders/languagesLoader' import enhanceLanguages from '../../enhancers/enhanceLanguages' import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetIds from '../../utils/useWidgetIds' +import useWidgets from '../../utils/useWidgets' import CardLanguages from '../cards/CardLanguages' const RouteLanguages = (props) => { - const widgetIds = useWidgetIds(props, languagesLoader, { + const widgets = useWidgets(props, languagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) @@ -18,9 +18,8 @@ const RouteLanguages = (props) => { return ( h(Fragment, {}, - widgetIds.map( - (widgetId) => { - const widget = props.widgets.value[widgetId] + widgets.map( + (widget) => { if (widget == null) return h('p', {}, 'empty') const domain = selectDomainsValue.byId(props, widget.variables.domainId) diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index eac899be..4f70fab3 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -4,13 +4,13 @@ import enhancePages from '../../enhancers/enhancePages' import pagesLoader from '../../loaders/pagesLoader' import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetIds from '../../utils/useWidgetIds' +import useWidgets from '../../utils/useWidgets' import CardPages from '../cards/CardPages' const RoutePages = (props) => { - const widgetIds = useWidgetIds(props, pagesLoader, { + const widgets = useWidgets(props, pagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) @@ -18,9 +18,8 @@ const RoutePages = (props) => { return ( h(Fragment, {}, - widgetIds.map( - (widgetId) => { - const widget = props.widgets.value[widgetId] + widgets.map( + (widget) => { if (widget == null) return h('p', {}, 'empty') const domain = selectDomainsValue.byId(props, widget.variables.domainId) diff --git a/src/ui/scripts/components/routes/RouteReferrers.js b/src/ui/scripts/components/routes/RouteReferrers.js index 99155969..41dc7ac7 100644 --- a/src/ui/scripts/components/routes/RouteReferrers.js +++ b/src/ui/scripts/components/routes/RouteReferrers.js @@ -4,13 +4,13 @@ import referrersLoader from '../../loaders/referrersLoader' import enhanceReferrers from '../../enhancers/enhanceReferrers' import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetIds from '../../utils/useWidgetIds' +import useWidgets from '../../utils/useWidgets' import CardReferrers from '../cards/CardReferrers' const RouteReferrers = (props) => { - const widgetIds = useWidgetIds(props, referrersLoader, { + const widgets = useWidgets(props, referrersLoader, { range: props.filter.range, sorting: props.filter.sorting }) @@ -18,9 +18,8 @@ const RouteReferrers = (props) => { return ( h(Fragment, {}, - widgetIds.map( - (widgetId) => { - const widget = props.widgets.value[widgetId] + widgets.map( + (widget) => { if (widget == null) return h('p', {}, 'empty') const domain = selectDomainsValue.byId(props, widget.variables.domainId) diff --git a/src/ui/scripts/components/routes/RouteSizes.js b/src/ui/scripts/components/routes/RouteSizes.js index 62056156..7f3ec7ed 100644 --- a/src/ui/scripts/components/routes/RouteSizes.js +++ b/src/ui/scripts/components/routes/RouteSizes.js @@ -4,13 +4,13 @@ import sizesLoader from '../../loaders/sizesLoader' import enhanceSizes from '../../enhancers/enhanceSizes' import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetIds from '../../utils/useWidgetIds' +import useWidgets from '../../utils/useWidgets' import CardSizes from '../cards/CardSizes' const RouteSizes = (props) => { - const widgetIds = useWidgetIds(props, sizesLoader, { + const widgets = useWidgets(props, sizesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.sizesType @@ -19,9 +19,8 @@ const RouteSizes = (props) => { return ( h(Fragment, {}, - widgetIds.map( - (widgetId) => { - const widget = props.widgets.value[widgetId] + widgets.map( + (widget) => { if (widget == null) return h('p', {}, 'empty') const domain = selectDomainsValue.byId(props, widget.variables.domainId) diff --git a/src/ui/scripts/components/routes/RouteSystems.js b/src/ui/scripts/components/routes/RouteSystems.js index aebcc68c..dbb6c8de 100644 --- a/src/ui/scripts/components/routes/RouteSystems.js +++ b/src/ui/scripts/components/routes/RouteSystems.js @@ -4,13 +4,13 @@ import systemsLoader from '../../loaders/systemsLoader' import enhanceSystems from '../../enhancers/enhanceSystems' import * as selectDomainsValue from '../../selectors/selectDomainsValue' import overviewRoute from '../../utils/overviewRoute' -import useWidgetIds from '../../utils/useWidgetIds' +import useWidgets from '../../utils/useWidgets' import CardSystems from '../cards/CardSystems' const RouteSystems = (props) => { - const widgetIds = useWidgetIds(props, systemsLoader, { + const widgets = useWidgets(props, systemsLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.systemsType @@ -19,9 +19,8 @@ const RouteSystems = (props) => { return ( h(Fragment, {}, - widgetIds.map( - (widgetId) => { - const widget = props.widgets.value[widgetId] + widgets.map( + (widget) => { if (widget == null) return h('p', {}, 'empty') const domain = selectDomainsValue.byId(props, widget.variables.domainId) diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 74926ea2..92c84646 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -1,39 +1,37 @@ import { createElement as h, Fragment } from 'react' -// import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' +import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' import viewsLoader from '../../loaders/viewsLoader' import enhanceViews from '../../enhancers/enhanceViews' import * as selectDomainsValue from '../../selectors/selectDomainsValue' -// import mergeViews from '../../utils/mergeViews' +import mergeViews from '../../utils/mergeViews' import overviewRoute from '../../utils/overviewRoute' -import useWidgetIds from '../../utils/useWidgetIds' +import useWidgets from '../../utils/useWidgets' import CardViews from '../cards/CardViews' const RouteViews = (props) => { - const widgetIds = useWidgetIds(props, viewsLoader, { + const widgets = useWidgets(props, viewsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) return ( h(Fragment, {}, - - // h(CardViews, { - // wide: true, - // headline: ({ - // [VIEWS_TYPE_UNIQUE]: 'Site Views', - // [VIEWS_TYPE_TOTAL]: 'Page Views' - // })[props.filter.viewsType], - // interval: props.filter.interval, - // loading: props.fetching, - // items: mergeViews(props) - // }), - - widgetIds.map( - (widgetId) => { - const widget = props.widgets.value[widgetId] + h(CardViews, { + wide: true, + headline: ({ + [VIEWS_TYPE_UNIQUE]: 'Site Views', + [VIEWS_TYPE_TOTAL]: 'Page Views' + })[props.filter.viewsType], + interval: props.filter.interval, + loading: props.fetching, + items: mergeViews(widgets) + }), + + widgets.map( + (widget) => { if (widget == null) return h('p', {}, 'empty') const domain = selectDomainsValue.byId(props, widget.variables.domainId) @@ -48,7 +46,6 @@ const RouteViews = (props) => { }) } ) - ) ) diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index a751b204..ca484bbb 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -8,7 +8,6 @@ export default (state) => { selectOverviewValue.withoutType(state, ALL_DOMAINS).fetching === true || Object.values(state.overview.value).some((value) => value.fetching) === true || Object.values(state.widgets.value).some((value) => value.fetching) === true || - state.durations.fetching === true || state.domains.fetching === true || state.token.fetching === true || state.permanentTokens.fetching === true || @@ -19,7 +18,6 @@ export default (state) => { selectOverviewValue.withoutType(state, ALL_DOMAINS).error, ...Object.values(state.overview.value).map((value) => value.error), ...Object.values(state.widgets.value).map((value) => value.error), - state.durations.error, state.domains.error, state.token.error, state.permanentTokens.error, diff --git a/src/ui/scripts/loaders/durationsLoader.js b/src/ui/scripts/loaders/durationsLoader.js new file mode 100644 index 00000000..bded2342 --- /dev/null +++ b/src/ui/scripts/loaders/durationsLoader.js @@ -0,0 +1,34 @@ +export default (domainId, opts) => { + + // TODO: Improve ids + const id = `fetchDurations${ domainId }${ JSON.stringify(opts) }` + + const query = ` + query fetchDurations($domainId: ID!, $interval: Interval!) { + domain(id: $domainId) { + id + statistics { + durations(interval: $interval) { + id + count + } + } + } + } + ` + + const variables = { + domainId, + interval: opts.interval + } + + const selector = (data) => data.domain.statistics.durations + + return { + id, + query, + variables, + selector + } + +} \ No newline at end of file diff --git a/src/ui/scripts/reducers/durations.js b/src/ui/scripts/reducers/durations.js deleted file mode 100644 index 941171ba..00000000 --- a/src/ui/scripts/reducers/durations.js +++ /dev/null @@ -1,35 +0,0 @@ -import produce from 'immer' - -import { - SET_DURATIONS_VALUE, - SET_DURATIONS_FETCHING, - SET_DURATIONS_ERROR -} from '../actions' - -import genericState from '../utils/genericState' -import genericSubState from '../utils/genericSubState' - -export const initialState = genericState - -export const initialSubState = genericSubState - -export default produce((draft, action) => { - - const hasDomainId = () => action.domainId != null - const hasDomainValue = () => draft.value[action.domainId] != null - - if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() - - switch (action.type) { - case SET_DURATIONS_VALUE: - draft.value[action.domainId].value = action.payload || initialSubState().value - break - case SET_DURATIONS_FETCHING: - draft.fetching = action.payload || initialState().fetching - break - case SET_DURATIONS_ERROR: - draft.error = action.payload || initialState().error - break - } - -}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/reducers/index.js b/src/ui/scripts/reducers/index.js index 9c65e522..85fdc79a 100644 --- a/src/ui/scripts/reducers/index.js +++ b/src/ui/scripts/reducers/index.js @@ -12,7 +12,6 @@ import filter from './filter' import domains from './domains' import events from './events' import overview from './overview' -import durations from './durations' import widgets from './widgets' const reducers = combineReducers({ @@ -24,7 +23,6 @@ const reducers = combineReducers({ domains, events, overview, - durations, widgets }) diff --git a/src/ui/scripts/selectors/selectDurationsValue.js b/src/ui/scripts/selectors/selectDurationsValue.js deleted file mode 100644 index e54696a9..00000000 --- a/src/ui/scripts/selectors/selectDurationsValue.js +++ /dev/null @@ -1,6 +0,0 @@ -import { initialSubState } from '../reducers/durations' - -export default (state, domainId) => { - const value = state.durations.value[domainId] - return value == null ? initialSubState() : value -} \ No newline at end of file diff --git a/src/ui/scripts/selectors/selectViewsValue.js b/src/ui/scripts/selectors/selectViewsValue.js deleted file mode 100644 index 695c38fd..00000000 --- a/src/ui/scripts/selectors/selectViewsValue.js +++ /dev/null @@ -1,7 +0,0 @@ -// TODO: Remove file -// import { initialSubState } from '../reducers/views' - -// export default (state, domainId) => { -// const value = state.views.value[domainId] -// return value == null ? initialSubState() : value -// } \ No newline at end of file diff --git a/src/ui/scripts/utils/mergeDurations.js b/src/ui/scripts/utils/mergeDurations.js index a7355e96..accd2e0d 100644 --- a/src/ui/scripts/utils/mergeDurations.js +++ b/src/ui/scripts/utils/mergeDurations.js @@ -1,15 +1,13 @@ -import selectDurationsValue from '../selectors/selectDurationsValue' +// import selectDurationsValue from '../selectors/selectDurationsValue' import enhanceDurations from '../enhancers/enhanceDurations' -// Turns the durations of multiple domains into one array of durations -export default (state) => { +// Turns the durations of multiple widgets into one array of durations +export default (widgets) => { - // Enhance durations for all domains - const enhancedDurations = state.domains.value.map((domain) => { + // Enhance durations for all widgets + const enhancedDurations = widgets.map((widget) => { - const duration = selectDurationsValue(state, domain.id) - - return enhanceDurations(duration.value, 14) + return enhanceDurations(widget.value, 14) }) diff --git a/src/ui/scripts/utils/mergeViews.js b/src/ui/scripts/utils/mergeViews.js index 9785b361..392684e5 100644 --- a/src/ui/scripts/utils/mergeViews.js +++ b/src/ui/scripts/utils/mergeViews.js @@ -1,15 +1,13 @@ -import selectViewsValue from '../selectors/selectViewsValue' import enhanceViews from '../enhancers/enhanceViews' -// Turns the views of multiple domains into one array of views -export default (state) => { +// Turns the views of multiple widgets into one array of views +export default (widgets) => { - // Enhance views for all domains - const enhancedViews = state.domains.value.map((domain) => { + // Enhance views for all widgets + const enhancedViews = widgets.map((widget) => { - const view = selectViewsValue(state, domain.id) - - return enhanceViews(view.value, 14) + console.log(widget) + return enhanceViews(widget.value, 14) }) diff --git a/src/ui/scripts/utils/useWidgetIds.js b/src/ui/scripts/utils/useWidgets.js similarity index 77% rename from src/ui/scripts/utils/useWidgetIds.js rename to src/ui/scripts/utils/useWidgets.js index c9a847a1..cb37f1dc 100644 --- a/src/ui/scripts/utils/useWidgetIds.js +++ b/src/ui/scripts/utils/useWidgets.js @@ -19,6 +19,9 @@ export default (props, createLoader, opts) => { }, [ props.domains.value, ...Object.values(opts) ]) - return widgetIds + return widgetIds.map((widgetId) => { + console.log(props.widgets.value[widgetId]) + return props.widgets.value[widgetId] + }) } \ No newline at end of file From a025753669235412c0a775815c6a313f0b833a3e Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 16:01:50 +0100 Subject: [PATCH 043/208] Reduce amount of state changes --- src/ui/scripts/actions/domains.js | 31 +++++++++---------- src/ui/scripts/actions/events.js | 31 +++++++++---------- src/ui/scripts/actions/token.js | 19 +++++++----- src/ui/scripts/actions/widgets.js | 23 ++++++-------- .../scripts/components/routes/RouteViews.js | 2 ++ src/ui/scripts/reducers/domains.js | 12 +++++-- src/ui/scripts/reducers/events.js | 12 +++++-- src/ui/scripts/reducers/token.js | 12 +++++-- src/ui/scripts/reducers/widgets.js | 14 +++++---- src/ui/scripts/utils/mergeDurations.js | 1 - 10 files changed, 86 insertions(+), 71 deletions(-) diff --git a/src/ui/scripts/actions/domains.js b/src/ui/scripts/actions/domains.js index bd0e83ed..4f6bd11e 100644 --- a/src/ui/scripts/actions/domains.js +++ b/src/ui/scripts/actions/domains.js @@ -1,13 +1,18 @@ import api from '../utils/api' import signalHandler from '../utils/signalHandler' -export const SET_DOMAINS_VALUE = Symbol() +export const SET_DOMAINS_START = Symbol() +export const SET_DOMAINS_END = Symbol() export const SET_DOMAINS_FETCHING = Symbol() export const SET_DOMAINS_ERROR = Symbol() -export const setDomainsValue = (payload) => ({ - type: SET_DOMAINS_VALUE, - payload +export const setDomainsStart = () => ({ + type: SET_DOMAINS_START +}) + +export const setDomainsEnd = (value) => ({ + type: SET_DOMAINS_END, + value }) export const setDomainsFetching = (payload) => ({ @@ -22,8 +27,7 @@ export const setDomainsError = (payload) => ({ export const fetchDomains = signalHandler((signal) => (props) => async (dispatch) => { - dispatch(setDomainsFetching(true)) - dispatch(setDomainsError()) + dispatch(setDomainsStart()) try { @@ -40,8 +44,7 @@ export const fetchDomains = signalHandler((signal) => (props) => async (dispatch signal: signal() }) - dispatch(setDomainsValue(data.domains)) - dispatch(setDomainsFetching(false)) + dispatch(setDomainsEnd(data.domains)) } catch (err) { @@ -56,8 +59,7 @@ export const fetchDomains = signalHandler((signal) => (props) => async (dispatch export const addDomain = (props, state) => async (dispatch) => { - dispatch(setDomainsFetching(true)) - dispatch(setDomainsError()) + dispatch(setDomainsStart()) try { @@ -78,7 +80,6 @@ export const addDomain = (props, state) => async (dispatch) => { }) await dispatch(fetchDomains(props)) - dispatch(setDomainsFetching(false)) } catch (err) { @@ -93,8 +94,7 @@ export const addDomain = (props, state) => async (dispatch) => { export const updateDomain = signalHandler((signal) => (props, domainId, state) => async (dispatch) => { - dispatch(setDomainsFetching(true)) - dispatch(setDomainsError()) + dispatch(setDomainsStart()) try { @@ -117,7 +117,6 @@ export const updateDomain = signalHandler((signal) => (props, domainId, state) = }) await dispatch(fetchDomains(props)) - dispatch(setDomainsFetching(false)) } catch (err) { @@ -132,8 +131,7 @@ export const updateDomain = signalHandler((signal) => (props, domainId, state) = export const deleteDomain = signalHandler((signal) => (props, domainId) => async (dispatch) => { - dispatch(setDomainsFetching(true)) - dispatch(setDomainsError()) + dispatch(setDomainsStart()) try { @@ -153,7 +151,6 @@ export const deleteDomain = signalHandler((signal) => (props, domainId) => async }) await dispatch(fetchDomains(props)) - dispatch(setDomainsFetching(false)) } catch (err) { diff --git a/src/ui/scripts/actions/events.js b/src/ui/scripts/actions/events.js index 07900665..ef5d71f9 100644 --- a/src/ui/scripts/actions/events.js +++ b/src/ui/scripts/actions/events.js @@ -1,13 +1,18 @@ import api from '../utils/api' import signalHandler from '../utils/signalHandler' -export const SET_EVENTS_VALUE = Symbol() +export const SET_EVENTS_START = Symbol() +export const SET_EVENTS_END = Symbol() export const SET_EVENTS_FETCHING = Symbol() export const SET_EVENTS_ERROR = Symbol() -export const setEventsValue = (payload) => ({ - type: SET_EVENTS_VALUE, - payload +export const setEventsStart = () => ({ + type: SET_EVENTS_START +}) + +export const setEventsEnd = (value) => ({ + type: SET_EVENTS_END, + value }) export const setEventsFetching = (payload) => ({ @@ -22,8 +27,7 @@ export const setEventsError = (payload) => ({ export const fetchEvents = signalHandler((signal) => (props) => async (dispatch) => { - dispatch(setEventsFetching(true)) - dispatch(setEventsError()) + dispatch(setEventsStart(true)) try { @@ -41,8 +45,7 @@ export const fetchEvents = signalHandler((signal) => (props) => async (dispatch) signal: signal() }) - dispatch(setEventsValue(data.events)) - dispatch(setEventsFetching(false)) + dispatch(setEventsEnd(data.events)) } catch (err) { @@ -57,8 +60,7 @@ export const fetchEvents = signalHandler((signal) => (props) => async (dispatch) export const addEvent = (props, state) => async (dispatch) => { - dispatch(setEventsFetching(true)) - dispatch(setEventsError()) + dispatch(setEventsStart(true)) try { @@ -80,7 +82,6 @@ export const addEvent = (props, state) => async (dispatch) => { }) await dispatch(fetchEvents(props)) - dispatch(setEventsFetching(false)) } catch (err) { @@ -95,8 +96,7 @@ export const addEvent = (props, state) => async (dispatch) => { export const updateEvent = signalHandler((signal) => (props, eventId, state) => async (dispatch) => { - dispatch(setEventsFetching(true)) - dispatch(setEventsError()) + dispatch(setEventsStart(true)) try { @@ -120,7 +120,6 @@ export const updateEvent = signalHandler((signal) => (props, eventId, state) => }) await dispatch(fetchEvents(props)) - dispatch(setEventsFetching(false)) } catch (err) { @@ -135,8 +134,7 @@ export const updateEvent = signalHandler((signal) => (props, eventId, state) => export const deleteEvent = signalHandler((signal) => (props, eventId) => async (dispatch) => { - dispatch(setEventsFetching(true)) - dispatch(setEventsError()) + dispatch(setEventsStart(true)) try { @@ -156,7 +154,6 @@ export const deleteEvent = signalHandler((signal) => (props, eventId) => async ( }) await dispatch(fetchEvents(props)) - dispatch(setEventsFetching(false)) } catch (err) { diff --git a/src/ui/scripts/actions/token.js b/src/ui/scripts/actions/token.js index 6530a165..bba498da 100644 --- a/src/ui/scripts/actions/token.js +++ b/src/ui/scripts/actions/token.js @@ -3,13 +3,18 @@ import signalHandler from '../utils/signalHandler' import { resetState } from './index' -export const SET_TOKEN_VALUE = Symbol() +export const SET_TOKEN_START = Symbol() +export const SET_TOKEN_END = Symbol() export const SET_TOKEN_FETCHING = Symbol() export const SET_TOKEN_ERROR = Symbol() -export const setTokenValue = (payload) => ({ - type: SET_TOKEN_VALUE, - payload +export const setTokenStart = () => ({ + type: SET_TOKEN_START +}) + +export const setTokenEnd = (value) => ({ + type: SET_TOKEN_END, + value }) export const setTokenFetching = (payload) => ({ @@ -24,8 +29,7 @@ export const setTokenError = (payload) => ({ export const addToken = signalHandler((signal) => (props, state) => async (dispatch) => { - dispatch(setTokenFetching(true)) - dispatch(setTokenError()) + dispatch(setTokenStart(true)) try { @@ -50,8 +54,7 @@ export const addToken = signalHandler((signal) => (props, state) => async (dispa }) // TODO: Maybe just store the id instead of the payload - dispatch(setTokenValue(data.createToken.payload)) - dispatch(setTokenFetching(false)) + dispatch(setTokenEnd(data.createToken.payload)) } catch (err) { diff --git a/src/ui/scripts/actions/widgets.js b/src/ui/scripts/actions/widgets.js index fc5f13fd..3013e5cb 100644 --- a/src/ui/scripts/actions/widgets.js +++ b/src/ui/scripts/actions/widgets.js @@ -1,21 +1,21 @@ import api from '../utils/api' import signalHandler from '../utils/signalHandler' -export const SET_WIDGETS_VALUE = Symbol() -export const SET_WIDGETS_VARIABLES = Symbol() +export const SET_WIDGETS_START = Symbol() +export const SET_WIDGETS_END = Symbol() export const SET_WIDGETS_FETCHING = Symbol() export const SET_WIDGETS_ERROR = Symbol() -export const setWidgetsValue = (id, payload) => ({ - type: SET_WIDGETS_VALUE, +export const setWidgetsStart = (id, variables) => ({ + type: SET_WIDGETS_START, id, - payload + variables }) -export const setWidgetsVariables = (id, payload) => ({ - type: SET_WIDGETS_VARIABLES, +export const setWidgetsEnd = (id, value) => ({ + type: SET_WIDGETS_END, id, - payload + value }) export const setWidgetsFetching = (id, payload) => ({ @@ -34,9 +34,7 @@ export const fetchWidget = signalHandler((signal) => (props, loader) => async (d const { id, query, variables, selector } = loader - dispatch(setWidgetsVariables(id, variables)) - dispatch(setWidgetsFetching(id, true)) - dispatch(setWidgetsError(id)) + dispatch(setWidgetsStart(id, variables)) try { @@ -47,8 +45,7 @@ export const fetchWidget = signalHandler((signal) => (props, loader) => async (d signal: signal(id) }) - dispatch(setWidgetsValue(id, selector(data))) - dispatch(setWidgetsFetching(id, false)) + dispatch(setWidgetsEnd(id, selector(data))) } catch (err) { diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 92c84646..2448f627 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -17,6 +17,8 @@ const RouteViews = (props) => { type: props.filter.viewsType }) + console.count('views') + return ( h(Fragment, {}, h(CardViews, { diff --git a/src/ui/scripts/reducers/domains.js b/src/ui/scripts/reducers/domains.js index f3c0dc0c..5781a362 100644 --- a/src/ui/scripts/reducers/domains.js +++ b/src/ui/scripts/reducers/domains.js @@ -1,7 +1,8 @@ import produce from 'immer' import { - SET_DOMAINS_VALUE, + SET_DOMAINS_START, + SET_DOMAINS_END, SET_DOMAINS_FETCHING, SET_DOMAINS_ERROR } from '../actions' @@ -15,8 +16,13 @@ export const initialState = () => ({ export default produce((draft, action) => { switch (action.type) { - case SET_DOMAINS_VALUE: - draft.value = action.payload || initialState().value + case SET_DOMAINS_START: + draft.fetching = true + draft.error = initialState().error + break + case SET_DOMAINS_END: + draft.value = action.value || initialState().value + draft.fetching = false break case SET_DOMAINS_FETCHING: draft.fetching = action.payload || initialState().fetching diff --git a/src/ui/scripts/reducers/events.js b/src/ui/scripts/reducers/events.js index 3df11af4..a21f2171 100644 --- a/src/ui/scripts/reducers/events.js +++ b/src/ui/scripts/reducers/events.js @@ -1,7 +1,8 @@ import produce from 'immer' import { - SET_EVENTS_VALUE, + SET_EVENTS_START, + SET_EVENTS_END, SET_EVENTS_FETCHING, SET_EVENTS_ERROR } from '../actions' @@ -15,8 +16,13 @@ export const initialState = () => ({ export default produce((draft, action) => { switch (action.type) { - case SET_EVENTS_VALUE: - draft.value = action.payload || initialState().value + case SET_EVENTS_START: + draft.fetching = true + draft.error = initialState().error + break + case SET_EVENTS_END: + draft.value = action.value || initialState().value + draft.fetching = false break case SET_EVENTS_FETCHING: draft.fetching = action.payload || initialState().fetching diff --git a/src/ui/scripts/reducers/token.js b/src/ui/scripts/reducers/token.js index 1637d723..92951e5e 100644 --- a/src/ui/scripts/reducers/token.js +++ b/src/ui/scripts/reducers/token.js @@ -1,7 +1,8 @@ import produce from 'immer' import { - SET_TOKEN_VALUE, + SET_TOKEN_START, + SET_TOKEN_END, SET_TOKEN_FETCHING, SET_TOKEN_ERROR } from '../actions' @@ -15,8 +16,13 @@ export const initialState = () => ({ export default produce((draft, action) => { switch (action.type) { - case SET_TOKEN_VALUE: - draft.value = action.payload || initialState().value + case SET_TOKEN_START: + draft.fetching = true + draft.error = initialState().error + break + case SET_TOKEN_END: + draft.value = action.value || initialState().value + draft.fetching = false break case SET_TOKEN_FETCHING: draft.fetching = action.payload || initialState().fetching diff --git a/src/ui/scripts/reducers/widgets.js b/src/ui/scripts/reducers/widgets.js index 7d15eb71..7be62c2d 100644 --- a/src/ui/scripts/reducers/widgets.js +++ b/src/ui/scripts/reducers/widgets.js @@ -1,8 +1,8 @@ import produce from 'immer' import { - SET_WIDGETS_VALUE, - SET_WIDGETS_VARIABLES, + SET_WIDGETS_START, + SET_WIDGETS_END, SET_WIDGETS_FETCHING, SET_WIDGETS_ERROR } from '../actions' @@ -26,11 +26,13 @@ export default produce((draft, action) => { if (hasId() === true && hasValue() === false) draft.value[action.id] = initialSubState() switch (action.type) { - case SET_WIDGETS_VALUE: - draft.value[action.id].value = action.payload || initialSubState().value + case SET_WIDGETS_START: + draft.value[action.id].variables = action.variables || initialSubState().variables + draft.value[action.id].fetching = true break - case SET_WIDGETS_VARIABLES: - draft.value[action.id].variables = action.payload || initialSubState().value + case SET_WIDGETS_END: + draft.value[action.id].value = action.value || initialSubState().value + draft.value[action.id].fetching = false break case SET_WIDGETS_FETCHING: draft.value[action.id].fetching = action.payload || initialSubState().fetching diff --git a/src/ui/scripts/utils/mergeDurations.js b/src/ui/scripts/utils/mergeDurations.js index accd2e0d..4264c43e 100644 --- a/src/ui/scripts/utils/mergeDurations.js +++ b/src/ui/scripts/utils/mergeDurations.js @@ -1,4 +1,3 @@ -// import selectDurationsValue from '../selectors/selectDurationsValue' import enhanceDurations from '../enhancers/enhanceDurations' // Turns the durations of multiple widgets into one array of durations From 99705364a20c4c43deedbbe6b3e3bb2d4e07d2d8 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 16:06:22 +0100 Subject: [PATCH 044/208] Reduce amount of set states for overview --- src/ui/scripts/actions/overview.js | 29 ++++++++++++----------------- src/ui/scripts/reducers/overview.js | 15 +++++++++------ 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/ui/scripts/actions/overview.js b/src/ui/scripts/actions/overview.js index 917128e1..0a9ea5bd 100644 --- a/src/ui/scripts/actions/overview.js +++ b/src/ui/scripts/actions/overview.js @@ -7,21 +7,21 @@ import { INTERVALS_DAILY } from '../../../constants/intervals' export const ALL_DOMAINS = Symbol() -export const SET_OVERVIEW_FACTS = Symbol() -export const SET_OVERVIEW_STATISTICS = Symbol() +export const SET_OVERVIEW_START = Symbol() +export const SET_OVERVIEW_END = Symbol() export const SET_OVERVIEW_FETCHING = Symbol() export const SET_OVERVIEW_ERROR = Symbol() -export const setOverviewFacts = (domainId, payload) => ({ - type: SET_OVERVIEW_FACTS, - domainId, - payload +export const setOverviewStart = (domainId) => ({ + type: SET_OVERVIEW_START, + domainId }) -export const setOverviewStatistics = (domainId, payload) => ({ - type: SET_OVERVIEW_STATISTICS, +export const setOverviewEnd = (domainId, facts, statistics) => ({ + type: SET_OVERVIEW_END, domainId, - payload + facts, + statistics }) export const setOverviewFetching = (domainId, payload) => ({ @@ -38,8 +38,7 @@ export const setOverviewError = (domainId, payload) => ({ export const fetchOverview = signalHandler((signal) => (props, domainId) => async (dispatch) => { - dispatch(setOverviewFetching(domainId, true)) - dispatch(setOverviewError(domainId)) + dispatch(setOverviewStart(domainId)) try { @@ -120,8 +119,7 @@ export const fetchOverview = signalHandler((signal) => (props, domainId) => asyn signal: signal(domainId) }) - dispatch(setOverviewFacts(domainId, data.facts)) - dispatch(setOverviewStatistics(domainId, data.statistics)) + dispatch(setOverviewEnd(domainId, data.facts, data.statistics)) } else { @@ -144,13 +142,10 @@ export const fetchOverview = signalHandler((signal) => (props, domainId) => asyn signal: signal(domainId) }) - dispatch(setOverviewFacts(domainId, data.domain.facts)) - dispatch(setOverviewStatistics(domainId, data.domain.statistics)) + dispatch(setOverviewEnd(domainId, data.domain.facts, data.domain.statistics)) } - dispatch(setOverviewFetching(domainId, false)) - } catch (err) { if (err.name === 'AbortError') return diff --git a/src/ui/scripts/reducers/overview.js b/src/ui/scripts/reducers/overview.js index d2bb84b5..8b13ef5d 100644 --- a/src/ui/scripts/reducers/overview.js +++ b/src/ui/scripts/reducers/overview.js @@ -1,8 +1,8 @@ import produce from 'immer' import { - SET_OVERVIEW_FACTS, - SET_OVERVIEW_STATISTICS, + SET_OVERVIEW_START, + SET_OVERVIEW_END, SET_OVERVIEW_FETCHING, SET_OVERVIEW_ERROR } from '../actions' @@ -26,11 +26,14 @@ export default produce((draft, action) => { if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() switch (action.type) { - case SET_OVERVIEW_FACTS: - draft.value[action.domainId].facts = action.payload || initialSubState().facts + case SET_OVERVIEW_START: + draft.value[action.domainId].fetching = true + draft.value[action.domainId].error = action.payload || initialSubState().error break - case SET_OVERVIEW_STATISTICS: - draft.value[action.domainId].statistics = action.payload || initialSubState().statistics + case SET_OVERVIEW_END: + draft.value[action.domainId].facts = action.facts || initialSubState().facts + draft.value[action.domainId].statistics = action.statistics || initialSubState().statistics + draft.value[action.domainId].fetching = false break case SET_OVERVIEW_FETCHING: draft.value[action.domainId].fetching = action.payload || initialSubState().fetching From be39c57367a7d7a21594d3329eae537ac108add8 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 16:10:56 +0100 Subject: [PATCH 045/208] Fallback when a widget isn't ready --- src/ui/scripts/utils/useWidgets.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/utils/useWidgets.js b/src/ui/scripts/utils/useWidgets.js index cb37f1dc..77030512 100644 --- a/src/ui/scripts/utils/useWidgets.js +++ b/src/ui/scripts/utils/useWidgets.js @@ -1,5 +1,7 @@ import { useEffect, useState } from 'react' +import { initialSubState } from '../reducers/widgets' + export default (props, createLoader, opts) => { const [ widgetIds, setWidgetIds ] = useState([]) @@ -20,8 +22,8 @@ export default (props, createLoader, opts) => { }, [ props.domains.value, ...Object.values(opts) ]) return widgetIds.map((widgetId) => { - console.log(props.widgets.value[widgetId]) - return props.widgets.value[widgetId] + const widget = props.widgets.value[widgetId] + return widget == null ? initialSubState() : widget }) } \ No newline at end of file From d1ee4a5499a98d1fb83d07a209847c7e1ed46f80 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 18:02:33 +0100 Subject: [PATCH 046/208] Refactor card rendering --- src/ui/scripts/actions/widgets.js | 7 +- .../scripts/components/cards/CardBrowsers.js | 24 +----- .../scripts/components/cards/CardDevices.js | 24 +----- .../scripts/components/cards/CardLanguages.js | 24 +----- src/ui/scripts/components/cards/CardPages.js | 24 +----- .../scripts/components/cards/CardReferrers.js | 22 +---- src/ui/scripts/components/cards/CardSizes.js | 24 +----- .../scripts/components/cards/CardSystems.js | 24 +----- src/ui/scripts/components/cards/CardWidget.js | 82 +++++++++++++++++++ .../components/routes/RouteBrowsers.js | 33 +------- .../scripts/components/routes/RouteDevices.js | 33 +------- .../components/routes/RouteDurations.js | 24 +----- .../components/routes/RouteLanguages.js | 33 +------- .../scripts/components/routes/RoutePages.js | 33 +------- .../components/routes/RouteReferrers.js | 33 +------- .../scripts/components/routes/RouteSizes.js | 33 +------- .../scripts/components/routes/RouteSystems.js | 33 +------- .../scripts/components/routes/RouteViews.js | 26 +----- src/ui/scripts/loaders/browsersLoader.js | 25 ++++++ src/ui/scripts/loaders/devicesLoader.js | 25 ++++++ src/ui/scripts/loaders/durationsLoader.js | 25 ++++++ src/ui/scripts/loaders/languagesLoader.js | 25 ++++++ src/ui/scripts/loaders/pagesLoader.js | 25 ++++++ src/ui/scripts/loaders/referrersLoader.js | 25 ++++++ src/ui/scripts/loaders/sizesLoader.js | 25 ++++++ src/ui/scripts/loaders/systemsLoader.js | 25 ++++++ src/ui/scripts/loaders/viewsLoader.js | 25 ++++++ src/ui/scripts/reducers/widgets.js | 2 + src/ui/scripts/utils/useWidgets.js | 26 +++++- 29 files changed, 391 insertions(+), 398 deletions(-) create mode 100644 src/ui/scripts/components/cards/CardWidget.js diff --git a/src/ui/scripts/actions/widgets.js b/src/ui/scripts/actions/widgets.js index 3013e5cb..838987e3 100644 --- a/src/ui/scripts/actions/widgets.js +++ b/src/ui/scripts/actions/widgets.js @@ -6,9 +6,10 @@ export const SET_WIDGETS_END = Symbol() export const SET_WIDGETS_FETCHING = Symbol() export const SET_WIDGETS_ERROR = Symbol() -export const setWidgetsStart = (id, variables) => ({ +export const setWidgetsStart = (id, Renderer, variables) => ({ type: SET_WIDGETS_START, id, + Renderer, variables }) @@ -32,9 +33,9 @@ export const setWidgetsError = (id, payload) => ({ export const fetchWidget = signalHandler((signal) => (props, loader) => async (dispatch) => { - const { id, query, variables, selector } = loader + const { id, Renderer, query, variables, selector } = loader - dispatch(setWidgetsStart(id, variables)) + dispatch(setWidgetsStart(id, Renderer, variables)) try { diff --git a/src/ui/scripts/components/cards/CardBrowsers.js b/src/ui/scripts/components/cards/CardBrowsers.js index 088837ba..9c55488a 100644 --- a/src/ui/scripts/components/cards/CardBrowsers.js +++ b/src/ui/scripts/components/cards/CardBrowsers.js @@ -8,10 +8,8 @@ import Text from '../Text' import Updating from '../Updating' import PresentationCounterList from '../presentations/PresentationCounterList' import PresentationList from '../presentations/PresentationList' -import PresentationEmptyState, { ICON_LOADING, ICON_WARNING } from '../presentations/PresentationEmptyState' import relativeDate from '../../utils/relativeDate' import rangeLabel from '../../utils/rangeLabel' -import status from '../../utils/status' const textLabel = (item, range, isRecent, isStale) => { @@ -31,32 +29,18 @@ const CardBrowsers = (props) => { const onEnter = (index) => setActive(index) const onLeave = () => setActive() - const { - isEmpty, - isStale, - isLoading - } = status(props.items, props.loading) - const presentation = (() => { - if (isLoading === true) return h(PresentationEmptyState, { - icon: ICON_LOADING - }, 'Loading browsers') - - if (isEmpty === false && props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { + if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { items: props.items }) - if (isEmpty === false && props.sorting === SORTINGS_RECENT) return h(PresentationList, { + if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { items: props.items, onEnter, onLeave }) - return h(PresentationEmptyState, { - icon: ICON_WARNING - }, 'No browsers') - })() return ( @@ -76,7 +60,7 @@ const CardBrowsers = (props) => { props.items[active], props.range, props.sorting === SORTINGS_RECENT, - isStale + props.stale )), presentation ) @@ -89,7 +73,7 @@ CardBrowsers.propTypes = { headline: PropTypes.string.isRequired, range: PropTypes.string.isRequired, sorting: PropTypes.string.isRequired, - loading: PropTypes.bool.isRequired, + stale: PropTypes.bool.isRequired, items: PropTypes.array.isRequired, onMore: PropTypes.func } diff --git a/src/ui/scripts/components/cards/CardDevices.js b/src/ui/scripts/components/cards/CardDevices.js index d0095edb..3e13ee69 100644 --- a/src/ui/scripts/components/cards/CardDevices.js +++ b/src/ui/scripts/components/cards/CardDevices.js @@ -8,10 +8,8 @@ import Text from '../Text' import Updating from '../Updating' import PresentationCounterList from '../presentations/PresentationCounterList' import PresentationList from '../presentations/PresentationList' -import PresentationEmptyState, { ICON_LOADING, ICON_WARNING } from '../presentations/PresentationEmptyState' import relativeDate from '../../utils/relativeDate' import rangeLabel from '../../utils/rangeLabel' -import status from '../../utils/status' const textLabel = (item, range, isRecent, isStale) => { @@ -30,32 +28,18 @@ const CardDevices = (props) => { const onEnter = (index) => setActive(index) const onLeave = () => setActive() - const { - isEmpty, - isStale, - isLoading - } = status(props.items, props.loading) - const presentation = (() => { - if (isLoading === true) return h(PresentationEmptyState, { - icon: ICON_LOADING - }, 'Loading devices') - - if (isEmpty === false && props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { + if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { items: props.items }) - if (isEmpty === false && props.sorting === SORTINGS_RECENT) return h(PresentationList, { + if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { items: props.items, onEnter, onLeave }) - return h(PresentationEmptyState, { - icon: ICON_WARNING - }, 'No device') - })() return ( @@ -75,7 +59,7 @@ const CardDevices = (props) => { props.items[active], props.range, props.sorting === SORTINGS_RECENT, - isStale + props.stale )), presentation ) @@ -88,7 +72,7 @@ CardDevices.propTypes = { headline: PropTypes.string.isRequired, range: PropTypes.string.isRequired, sorting: PropTypes.string.isRequired, - loading: PropTypes.bool.isRequired, + stale: PropTypes.bool.isRequired, items: PropTypes.array.isRequired, onMore: PropTypes.func } diff --git a/src/ui/scripts/components/cards/CardLanguages.js b/src/ui/scripts/components/cards/CardLanguages.js index 5534f2c5..8044b519 100644 --- a/src/ui/scripts/components/cards/CardLanguages.js +++ b/src/ui/scripts/components/cards/CardLanguages.js @@ -8,10 +8,8 @@ import Text from '../Text' import Updating from '../Updating' import PresentationCounterList from '../presentations/PresentationCounterList' import PresentationList from '../presentations/PresentationList' -import PresentationEmptyState, { ICON_LOADING, ICON_WARNING } from '../presentations/PresentationEmptyState' import relativeDate from '../../utils/relativeDate' import rangeLabel from '../../utils/rangeLabel' -import status from '../../utils/status' const textLabel = (item, range, isRecent, isStale) => { @@ -31,32 +29,18 @@ const CardLanguages = (props) => { const onEnter = (index) => setActive(index) const onLeave = () => setActive() - const { - isEmpty, - isStale, - isLoading - } = status(props.items, props.loading) - const presentation = (() => { - if (isLoading === true) return h(PresentationEmptyState, { - icon: ICON_LOADING - }, 'Loading languages') - - if (isEmpty === false && props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { + if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { items: props.items }) - if (isEmpty === false && props.sorting === SORTINGS_RECENT) return h(PresentationList, { + if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { items: props.items, onEnter, onLeave }) - return h(PresentationEmptyState, { - icon: ICON_WARNING - }, 'No languages') - })() return ( @@ -76,7 +60,7 @@ const CardLanguages = (props) => { props.items[active], props.range, props.sorting === SORTINGS_RECENT, - isStale + props.stale )), presentation ) @@ -89,7 +73,7 @@ CardLanguages.propTypes = { headline: PropTypes.string.isRequired, range: PropTypes.string.isRequired, sorting: PropTypes.string.isRequired, - loading: PropTypes.bool.isRequired, + stale: PropTypes.bool.isRequired, items: PropTypes.array.isRequired, onMore: PropTypes.func } diff --git a/src/ui/scripts/components/cards/CardPages.js b/src/ui/scripts/components/cards/CardPages.js index 5b665da4..ecccf18d 100644 --- a/src/ui/scripts/components/cards/CardPages.js +++ b/src/ui/scripts/components/cards/CardPages.js @@ -8,10 +8,8 @@ import Text from '../Text' import Updating from '../Updating' import PresentationCounterList from '../presentations/PresentationCounterList' import PresentationList from '../presentations/PresentationList' -import PresentationEmptyState, { ICON_LOADING, ICON_WARNING } from '../presentations/PresentationEmptyState' import relativeDate from '../../utils/relativeDate' import rangeLabel from '../../utils/rangeLabel' -import status from '../../utils/status' const textLabel = (item, range, isRecent, isStale) => { @@ -31,32 +29,18 @@ const CardPages = (props) => { const onEnter = (index) => setActive(index) const onLeave = () => setActive() - const { - isEmpty, - isStale, - isLoading - } = status(props.items, props.loading) - const presentation = (() => { - if (isLoading === true) return h(PresentationEmptyState, { - icon: ICON_LOADING - }, 'Loading pages') - - if (isEmpty === false && props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { + if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { items: props.items }) - if (isEmpty === false && props.sorting === SORTINGS_RECENT) return h(PresentationList, { + if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { items: props.items, onEnter, onLeave }) - return h(PresentationEmptyState, { - icon: ICON_WARNING - }, 'No pages') - })() return ( @@ -76,7 +60,7 @@ const CardPages = (props) => { props.items[active], props.range, props.sorting === SORTINGS_RECENT, - isStale + props.stale )), presentation ) @@ -89,7 +73,7 @@ CardPages.propTypes = { headline: PropTypes.string.isRequired, range: PropTypes.string.isRequired, sorting: PropTypes.string.isRequired, - loading: PropTypes.bool.isRequired, + stale: PropTypes.bool.isRequired, items: PropTypes.array.isRequired, onMore: PropTypes.func } diff --git a/src/ui/scripts/components/cards/CardReferrers.js b/src/ui/scripts/components/cards/CardReferrers.js index 945dbd9e..78456c5e 100644 --- a/src/ui/scripts/components/cards/CardReferrers.js +++ b/src/ui/scripts/components/cards/CardReferrers.js @@ -7,10 +7,8 @@ import Headline from '../Headline' import Text from '../Text' import Updating from '../Updating' import PresentationIconList from '../presentations/PresentationIconList' -import PresentationEmptyState, { ICON_LOADING, ICON_WARNING } from '../presentations/PresentationEmptyState' import relativeDate from '../../utils/relativeDate' import rangeLabel from '../../utils/rangeLabel' -import status from '../../utils/status' const textLabel = (item, range, isRecent, isNew, isStale) => { @@ -34,28 +32,14 @@ const CardReferrers = (props) => { const onEnter = (index) => setActive(index) const onLeave = () => setActive() - const { - isEmpty, - isStale, - isLoading - } = status(props.items, props.loading) - const presentation = (() => { - if (isLoading === true) return h(PresentationEmptyState, { - icon: ICON_LOADING - }, 'Loading referrers') - - if (isEmpty === false) return h(PresentationIconList, { + return h(PresentationIconList, { items: props.items, onEnter, onLeave }) - return h(PresentationEmptyState, { - icon: ICON_WARNING - }, 'No referrers') - })() return ( @@ -76,7 +60,7 @@ const CardReferrers = (props) => { props.range, props.sorting === SORTINGS_RECENT, props.sorting === SORTINGS_NEW, - isStale + props.stale )), presentation ) @@ -88,7 +72,7 @@ const CardReferrers = (props) => { CardReferrers.propTypes = { headline: PropTypes.string.isRequired, range: PropTypes.string.isRequired, - loading: PropTypes.bool.isRequired, + stale: PropTypes.bool.isRequired, items: PropTypes.array.isRequired, onMore: PropTypes.func } diff --git a/src/ui/scripts/components/cards/CardSizes.js b/src/ui/scripts/components/cards/CardSizes.js index 89454e5b..9828515e 100644 --- a/src/ui/scripts/components/cards/CardSizes.js +++ b/src/ui/scripts/components/cards/CardSizes.js @@ -8,10 +8,8 @@ import Text from '../Text' import Updating from '../Updating' import PresentationCounterList from '../presentations/PresentationCounterList' import PresentationList from '../presentations/PresentationList' -import PresentationEmptyState, { ICON_LOADING, ICON_WARNING } from '../presentations/PresentationEmptyState' import relativeDate from '../../utils/relativeDate' import rangeLabel from '../../utils/rangeLabel' -import status from '../../utils/status' const textLabel = (item, range, isRecent, isStale) => { @@ -31,32 +29,18 @@ const CardSizes = (props) => { const onEnter = (index) => setActive(index) const onLeave = () => setActive() - const { - isEmpty, - isStale, - isLoading - } = status(props.items, props.loading) - const presentation = (() => { - if (isLoading === true) return h(PresentationEmptyState, { - icon: ICON_LOADING - }, 'Loading sizes') - - if (isEmpty === false && props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { + if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { items: props.items }) - if (isEmpty === false && props.sorting === SORTINGS_RECENT) return h(PresentationList, { + if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { items: props.items, onEnter, onLeave }) - return h(PresentationEmptyState, { - icon: ICON_WARNING - }, 'No sizes') - })() return ( @@ -76,7 +60,7 @@ const CardSizes = (props) => { props.items[active], props.range, props.sorting === SORTINGS_RECENT, - isStale + props.stale )), presentation ) @@ -89,7 +73,7 @@ CardSizes.propTypes = { headline: PropTypes.string.isRequired, range: PropTypes.string.isRequired, sorting: PropTypes.string.isRequired, - loading: PropTypes.bool.isRequired, + stale: PropTypes.bool.isRequired, items: PropTypes.array.isRequired, onMore: PropTypes.func } diff --git a/src/ui/scripts/components/cards/CardSystems.js b/src/ui/scripts/components/cards/CardSystems.js index 32c74908..d36c70fe 100644 --- a/src/ui/scripts/components/cards/CardSystems.js +++ b/src/ui/scripts/components/cards/CardSystems.js @@ -8,10 +8,8 @@ import Text from '../Text' import Updating from '../Updating' import PresentationCounterList from '../presentations/PresentationCounterList' import PresentationList from '../presentations/PresentationList' -import PresentationEmptyState, { ICON_LOADING, ICON_WARNING } from '../presentations/PresentationEmptyState' import relativeDate from '../../utils/relativeDate' import rangeLabel from '../../utils/rangeLabel' -import status from '../../utils/status' const textLabel = (item, range, isRecent, isStale) => { @@ -31,32 +29,18 @@ const CardSystems = (props) => { const onEnter = (index) => setActive(index) const onLeave = () => setActive() - const { - isEmpty, - isStale, - isLoading - } = status(props.items, props.loading) - const presentation = (() => { - if (isLoading === true) return h(PresentationEmptyState, { - icon: ICON_LOADING - }, 'Loading systems') - - if (isEmpty === false && props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { + if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { items: props.items }) - if (isEmpty === false && props.sorting === SORTINGS_RECENT) return h(PresentationList, { + if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { items: props.items, onEnter, onLeave }) - return h(PresentationEmptyState, { - icon: ICON_WARNING - }, 'No systems') - })() return ( @@ -76,7 +60,7 @@ const CardSystems = (props) => { props.items[active], props.range, props.sorting === SORTINGS_RECENT, - isStale + props.stale )), presentation ) @@ -89,7 +73,7 @@ CardSystems.propTypes = { headline: PropTypes.string.isRequired, range: PropTypes.string.isRequired, sorting: PropTypes.string.isRequired, - loading: PropTypes.bool.isRequired, + stale: PropTypes.bool.isRequired, items: PropTypes.array.isRequired, onMore: PropTypes.func } diff --git a/src/ui/scripts/components/cards/CardWidget.js b/src/ui/scripts/components/cards/CardWidget.js new file mode 100644 index 00000000..90146f22 --- /dev/null +++ b/src/ui/scripts/components/cards/CardWidget.js @@ -0,0 +1,82 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import Headline from '../Headline' +import Text from '../Text' +import PresentationEmptyState, { ICON_LOADING, ICON_WARNING } from '../presentations/PresentationEmptyState' +import status from '../../utils/status' + +const CardWidget = (props) => { + + const { + isEmpty, + isStale, + isLoading + } = status(props.widget.value, props.widget.fetching) + + if (isLoading === true) { + + return ( + h('div', { + className: 'card' + }, + h('div', { className: 'card__inner' }, + h(Headline, { + type: 'h2', + size: 'medium', + onClick: props.onMore + }, props.headline), + h(Text, { + type: 'div', + spacing: false + }, 'Test'), + h(PresentationEmptyState, { + icon: ICON_LOADING + }, 'Loading data') + ) + ) + ) + + } + + if (isEmpty === true) { + + return ( + h('div', { + className: 'card' + }, + h('div', { className: 'card__inner' }, + h(Headline, { + type: 'h2', + size: 'medium', + onClick: props.onMore + }, props.headline), + h(Text, { + type: 'div', + spacing: false + }, 'Test'), + h(PresentationEmptyState, { + icon: ICON_WARNING + }, 'No data') + ) + ) + ) + + } + + return h(props.widget.Renderer, { + headline: props.headline, + widget: props.widget, + stale: isStale, + onMore: props.onMore + }) + +} + +CardWidget.propTypes = { + headline: PropTypes.string.isRequired, + widget: PropTypes.object.isRequired, + onMore: PropTypes.func +} + +export default CardWidget \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteBrowsers.js b/src/ui/scripts/components/routes/RouteBrowsers.js index 3570781d..a767c7ca 100644 --- a/src/ui/scripts/components/routes/RouteBrowsers.js +++ b/src/ui/scripts/components/routes/RouteBrowsers.js @@ -1,44 +1,17 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h } from 'react' import browsersLoader from '../../loaders/browsersLoader' -import enhanceBrowsers from '../../enhancers/enhanceBrowsers' -import * as selectDomainsValue from '../../selectors/selectDomainsValue' -import overviewRoute from '../../utils/overviewRoute' import useWidgets from '../../utils/useWidgets' -import CardBrowsers from '../cards/CardBrowsers' - const RouteBrowsers = (props) => { - const widgets = useWidgets(props, browsersLoader, { + const { renderedWidgets } = useWidgets(props, browsersLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.browsersType }) - return ( - h(Fragment, {}, - - widgets.map( - (widget) => { - if (widget == null) return h('p', {}, 'empty') - - const domain = selectDomainsValue.byId(props, widget.variables.domainId) - - return h(CardBrowsers, { - key: domain.id, - headline: domain.title, - range: widget.variables.range, - sorting: widget.variables.sorting, - loading: widget.fetching, - items: enhanceBrowsers(widget.value), - onMore: () => props.setRoute(overviewRoute(domain)) - }) - } - ) - - ) - ) + return renderedWidgets } diff --git a/src/ui/scripts/components/routes/RouteDevices.js b/src/ui/scripts/components/routes/RouteDevices.js index 590d2336..a611a801 100644 --- a/src/ui/scripts/components/routes/RouteDevices.js +++ b/src/ui/scripts/components/routes/RouteDevices.js @@ -1,44 +1,17 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h } from 'react' import devicesLoader from '../../loaders/devicesLoader' -import enhanceDevices from '../../enhancers/enhanceDevices' -import * as selectDomainsValue from '../../selectors/selectDomainsValue' -import overviewRoute from '../../utils/overviewRoute' import useWidgets from '../../utils/useWidgets' -import CardDevices from '../cards/CardDevices' - const RouteDevices = (props) => { - const widgets = useWidgets(props, devicesLoader, { + const { renderedWidgets } = useWidgets(props, devicesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.devicesType }) - return ( - h(Fragment, {}, - - widgets.map( - (widget) => { - if (widget == null) return h('p', {}, 'empty') - - const domain = selectDomainsValue.byId(props, widget.variables.domainId) - - return h(CardDevices, { - key: domain.id, - headline: domain.title, - range: widget.variables.range, - sorting: widget.variables.sorting, - loading: widget.fetching, - items: enhanceDevices(widget.value), - onMore: () => props.setRoute(overviewRoute(domain)) - }) - } - ) - - ) - ) + return renderedWidgets } diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index a9b3ee8a..68777797 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -1,17 +1,14 @@ import { createElement as h, Fragment } from 'react' import durationsLoader from '../../loaders/durationsLoader' -import enhanceDurations from '../../enhancers/enhanceDurations' -import * as selectDomainsValue from '../../selectors/selectDomainsValue' import mergeDurations from '../../utils/mergeDurations' -import overviewRoute from '../../utils/overviewRoute' import useWidgets from '../../utils/useWidgets' import CardDurations from '../cards/CardDurations' const RouteDurations = (props) => { - const widgets = useWidgets(props, durationsLoader, { + const { rawWidgets, renderedWidgets } = useWidgets(props, durationsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) @@ -23,25 +20,10 @@ const RouteDurations = (props) => { headline: 'Durations', interval: props.filter.interval, loading: props.fetching, - items: mergeDurations(widgets) + items: mergeDurations(rawWidgets) }), - widgets.map( - (widget) => { - if (widget == null) return h('p', {}, 'empty') - - const domain = selectDomainsValue.byId(props, widget.variables.domainId) - - return h(CardDurations, { - key: domain.id, - headline: domain.title, - interval: widget.variables.interval, - loading: widget.fetching, - items: enhanceDurations(widget.value, 7), - onMore: () => props.setRoute(overviewRoute(domain)) - }) - } - ) + renderedWidgets ) ) diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index 6aae82dc..8ec36ea9 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -1,43 +1,16 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h } from 'react' import languagesLoader from '../../loaders/languagesLoader' -import enhanceLanguages from '../../enhancers/enhanceLanguages' -import * as selectDomainsValue from '../../selectors/selectDomainsValue' -import overviewRoute from '../../utils/overviewRoute' import useWidgets from '../../utils/useWidgets' -import CardLanguages from '../cards/CardLanguages' - const RouteLanguages = (props) => { - const widgets = useWidgets(props, languagesLoader, { + const { renderedWidgets } = useWidgets(props, languagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) - return ( - h(Fragment, {}, - - widgets.map( - (widget) => { - if (widget == null) return h('p', {}, 'empty') - - const domain = selectDomainsValue.byId(props, widget.variables.domainId) - - return h(CardLanguages, { - key: domain.id, - headline: domain.title, - range: widget.variables.range, - sorting: widget.variables.sorting, - loading: widget.fetching, - items: enhanceLanguages(widget.value), - onMore: () => props.setRoute(overviewRoute(domain)) - }) - } - ) - - ) - ) + return renderedWidgets } diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index 4f70fab3..0339449e 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -1,43 +1,16 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h } from 'react' -import enhancePages from '../../enhancers/enhancePages' import pagesLoader from '../../loaders/pagesLoader' -import * as selectDomainsValue from '../../selectors/selectDomainsValue' -import overviewRoute from '../../utils/overviewRoute' import useWidgets from '../../utils/useWidgets' -import CardPages from '../cards/CardPages' - const RoutePages = (props) => { - const widgets = useWidgets(props, pagesLoader, { + const { renderedWidgets } = useWidgets(props, pagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) - return ( - h(Fragment, {}, - - widgets.map( - (widget) => { - if (widget == null) return h('p', {}, 'empty') - - const domain = selectDomainsValue.byId(props, widget.variables.domainId) - - return h(CardPages, { - key: domain.id, - headline: domain.title, - range: widget.variables.range, - sorting: widget.variables.sorting, - loading: widget.fetching, - items: enhancePages(widget.value), - onMore: () => props.setRoute(overviewRoute(domain)) - }) - } - ) - - ) - ) + return renderedWidgets } diff --git a/src/ui/scripts/components/routes/RouteReferrers.js b/src/ui/scripts/components/routes/RouteReferrers.js index 41dc7ac7..0dd8b575 100644 --- a/src/ui/scripts/components/routes/RouteReferrers.js +++ b/src/ui/scripts/components/routes/RouteReferrers.js @@ -1,43 +1,16 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h } from 'react' import referrersLoader from '../../loaders/referrersLoader' -import enhanceReferrers from '../../enhancers/enhanceReferrers' -import * as selectDomainsValue from '../../selectors/selectDomainsValue' -import overviewRoute from '../../utils/overviewRoute' import useWidgets from '../../utils/useWidgets' -import CardReferrers from '../cards/CardReferrers' - const RouteReferrers = (props) => { - const widgets = useWidgets(props, referrersLoader, { + const { renderedWidgets } = useWidgets(props, referrersLoader, { range: props.filter.range, sorting: props.filter.sorting }) - return ( - h(Fragment, {}, - - widgets.map( - (widget) => { - if (widget == null) return h('p', {}, 'empty') - - const domain = selectDomainsValue.byId(props, widget.variables.domainId) - - return h(CardReferrers, { - key: domain.id, - headline: domain.title, - range: widget.variables.range, - sorting: widget.variables.sorting, - loading: widget.fetching, - items: enhanceReferrers(widget.value), - onMore: () => props.setRoute(overviewRoute(domain)) - }) - } - ) - - ) - ) + return renderedWidgets } diff --git a/src/ui/scripts/components/routes/RouteSizes.js b/src/ui/scripts/components/routes/RouteSizes.js index 7f3ec7ed..da12857f 100644 --- a/src/ui/scripts/components/routes/RouteSizes.js +++ b/src/ui/scripts/components/routes/RouteSizes.js @@ -1,44 +1,17 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h } from 'react' import sizesLoader from '../../loaders/sizesLoader' -import enhanceSizes from '../../enhancers/enhanceSizes' -import * as selectDomainsValue from '../../selectors/selectDomainsValue' -import overviewRoute from '../../utils/overviewRoute' import useWidgets from '../../utils/useWidgets' -import CardSizes from '../cards/CardSizes' - const RouteSizes = (props) => { - const widgets = useWidgets(props, sizesLoader, { + const { renderedWidgets } = useWidgets(props, sizesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.sizesType }) - return ( - h(Fragment, {}, - - widgets.map( - (widget) => { - if (widget == null) return h('p', {}, 'empty') - - const domain = selectDomainsValue.byId(props, widget.variables.domainId) - - return h(CardSizes, { - key: domain.id, - headline: domain.title, - range: widget.variables.range, - sorting: widget.variables.sorting, - loading: widget.fetching, - items: enhanceSizes(widget.value), - onMore: () => props.setRoute(overviewRoute(domain)) - }) - } - ) - - ) - ) + return renderedWidgets } diff --git a/src/ui/scripts/components/routes/RouteSystems.js b/src/ui/scripts/components/routes/RouteSystems.js index dbb6c8de..e6819f12 100644 --- a/src/ui/scripts/components/routes/RouteSystems.js +++ b/src/ui/scripts/components/routes/RouteSystems.js @@ -1,44 +1,17 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h } from 'react' import systemsLoader from '../../loaders/systemsLoader' -import enhanceSystems from '../../enhancers/enhanceSystems' -import * as selectDomainsValue from '../../selectors/selectDomainsValue' -import overviewRoute from '../../utils/overviewRoute' import useWidgets from '../../utils/useWidgets' -import CardSystems from '../cards/CardSystems' - const RouteSystems = (props) => { - const widgets = useWidgets(props, systemsLoader, { + const { renderedWidgets } = useWidgets(props, systemsLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.systemsType }) - return ( - h(Fragment, {}, - - widgets.map( - (widget) => { - if (widget == null) return h('p', {}, 'empty') - - const domain = selectDomainsValue.byId(props, widget.variables.domainId) - - return h(CardSystems, { - key: domain.id, - headline: domain.title, - range: widget.variables.range, - sorting: widget.variables.sorting, - loading: widget.fetching, - items: enhanceSystems(widget.value), - onMore: () => props.setRoute(overviewRoute(domain)) - }) - } - ) - - ) - ) + return renderedWidgets } diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 2448f627..603bd5f8 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -2,23 +2,18 @@ import { createElement as h, Fragment } from 'react' import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' import viewsLoader from '../../loaders/viewsLoader' -import enhanceViews from '../../enhancers/enhanceViews' -import * as selectDomainsValue from '../../selectors/selectDomainsValue' import mergeViews from '../../utils/mergeViews' -import overviewRoute from '../../utils/overviewRoute' import useWidgets from '../../utils/useWidgets' import CardViews from '../cards/CardViews' const RouteViews = (props) => { - const widgets = useWidgets(props, viewsLoader, { + const { rawWidgets, renderedWidgets } = useWidgets(props, viewsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) - console.count('views') - return ( h(Fragment, {}, h(CardViews, { @@ -29,25 +24,10 @@ const RouteViews = (props) => { })[props.filter.viewsType], interval: props.filter.interval, loading: props.fetching, - items: mergeViews(widgets) + items: mergeViews(rawWidgets) }), - widgets.map( - (widget) => { - if (widget == null) return h('p', {}, 'empty') - - const domain = selectDomainsValue.byId(props, widget.variables.domainId) - - return h(CardViews, { - key: domain.id, - headline: domain.title, - interval: widget.variables.interval, - loading: widget.fetching, - items: enhanceViews(widget.value, 7), - onMore: () => props.setRoute(overviewRoute(domain)) - }) - } - ) + renderedWidgets ) ) diff --git a/src/ui/scripts/loaders/browsersLoader.js b/src/ui/scripts/loaders/browsersLoader.js index 854a5471..d2558489 100644 --- a/src/ui/scripts/loaders/browsersLoader.js +++ b/src/ui/scripts/loaders/browsersLoader.js @@ -1,3 +1,27 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import CardBrowsers from '../components/cards/CardBrowsers' +import enhanceBrowsers from '../enhancers/enhanceBrowsers' + +const Renderer = (props) => { + return h(CardBrowsers, { + headline: props.headline, + range: props.widget.variables.range, + sorting: props.widget.variables.sorting, + stale: props.stale, + items: enhanceBrowsers(props.widget.value), + onMore: props.onMore + }) +} + +Renderer.propTypes = { + headline: PropTypes.string.isRequired, + widget: PropTypes.object.isRequired, + stale: PropTypes.bool.isRequired, + onMore: PropTypes.func +} + export default (domainId, opts) => { // TODO: Improve ids @@ -29,6 +53,7 @@ export default (domainId, opts) => { return { id, + Renderer, query, variables, selector diff --git a/src/ui/scripts/loaders/devicesLoader.js b/src/ui/scripts/loaders/devicesLoader.js index 5aa7f4ab..1bc06a78 100644 --- a/src/ui/scripts/loaders/devicesLoader.js +++ b/src/ui/scripts/loaders/devicesLoader.js @@ -1,3 +1,27 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import CardDevices from '../components/cards/CardDevices' +import enhanceDevices from '../enhancers/enhanceDevices' + +const Renderer = (props) => { + return h(CardDevices, { + headline: props.headline, + range: props.widget.variables.range, + sorting: props.widget.variables.sorting, + stale: props.stale, + items: enhanceDevices(props.widget.value), + onMore: props.onMore + }) +} + +Renderer.propTypes = { + headline: PropTypes.string.isRequired, + widget: PropTypes.object.isRequired, + stale: PropTypes.bool.isRequired, + onMore: PropTypes.func +} + export default (domainId, opts) => { // TODO: Improve ids @@ -29,6 +53,7 @@ export default (domainId, opts) => { return { id, + Renderer, query, variables, selector diff --git a/src/ui/scripts/loaders/durationsLoader.js b/src/ui/scripts/loaders/durationsLoader.js index bded2342..2c28b155 100644 --- a/src/ui/scripts/loaders/durationsLoader.js +++ b/src/ui/scripts/loaders/durationsLoader.js @@ -1,3 +1,27 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import CardDurations from '../components/cards/CardDurations' +import enhanceDurations from '../enhancers/enhanceDurations' + +const Renderer = (props) => { + return h(CardDurations, { + headline: props.headline, + interval: props.widget.variables.interval, + sorting: props.widget.variables.sorting, + stale: props.stale, + items: enhanceDurations(props.widget.value, 7), + onMore: props.onMore + }) +} + +Renderer.propTypes = { + headline: PropTypes.string.isRequired, + widget: PropTypes.object.isRequired, + stale: PropTypes.bool.isRequired, + onMore: PropTypes.func +} + export default (domainId, opts) => { // TODO: Improve ids @@ -26,6 +50,7 @@ export default (domainId, opts) => { return { id, + Renderer, query, variables, selector diff --git a/src/ui/scripts/loaders/languagesLoader.js b/src/ui/scripts/loaders/languagesLoader.js index 6af75723..7f7aae9e 100644 --- a/src/ui/scripts/loaders/languagesLoader.js +++ b/src/ui/scripts/loaders/languagesLoader.js @@ -1,3 +1,27 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import CardLanguages from '../components/cards/CardLanguages' +import enhanceLanguages from '../enhancers/enhanceLanguages' + +const Renderer = (props) => { + return h(CardLanguages, { + headline: props.headline, + range: props.widget.variables.range, + sorting: props.widget.variables.sorting, + stale: props.stale, + items: enhanceLanguages(props.widget.value), + onMore: props.onMore + }) +} + +Renderer.propTypes = { + headline: PropTypes.string.isRequired, + widget: PropTypes.object.isRequired, + stale: PropTypes.bool.isRequired, + onMore: PropTypes.func +} + export default (domainId, opts) => { // TODO: Improve ids @@ -28,6 +52,7 @@ export default (domainId, opts) => { return { id, + Renderer, query, variables, selector diff --git a/src/ui/scripts/loaders/pagesLoader.js b/src/ui/scripts/loaders/pagesLoader.js index 2011abfa..e0db25e2 100644 --- a/src/ui/scripts/loaders/pagesLoader.js +++ b/src/ui/scripts/loaders/pagesLoader.js @@ -1,3 +1,27 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import CardPages from '../components/cards/CardPages' +import enhancePages from '../enhancers/enhancePages' + +const Renderer = (props) => { + return h(CardPages, { + headline: props.headline, + range: props.widget.variables.range, + sorting: props.widget.variables.sorting, + stale: props.stale, + items: enhancePages(props.widget.value), + onMore: props.onMore + }) +} + +Renderer.propTypes = { + headline: PropTypes.string.isRequired, + widget: PropTypes.object.isRequired, + stale: PropTypes.bool.isRequired, + onMore: PropTypes.func +} + export default (domainId, opts) => { // TODO: Improve ids @@ -28,6 +52,7 @@ export default (domainId, opts) => { return { id, + Renderer, query, variables, selector diff --git a/src/ui/scripts/loaders/referrersLoader.js b/src/ui/scripts/loaders/referrersLoader.js index 3001fb26..a93c42c2 100644 --- a/src/ui/scripts/loaders/referrersLoader.js +++ b/src/ui/scripts/loaders/referrersLoader.js @@ -1,3 +1,27 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import CardReferrers from '../components/cards/CardReferrers' +import enhanceReferrers from '../enhancers/enhanceReferrers' + +const Renderer = (props) => { + return h(CardReferrers, { + headline: props.headline, + range: props.widget.variables.range, + sorting: props.widget.variables.sorting, + stale: props.stale, + items: enhanceReferrers(props.widget.value), + onMore: props.onMore + }) +} + +Renderer.propTypes = { + headline: PropTypes.string.isRequired, + widget: PropTypes.object.isRequired, + stale: PropTypes.bool.isRequired, + onMore: PropTypes.func +} + export default (domainId, opts) => { // TODO: Improve ids @@ -28,6 +52,7 @@ export default (domainId, opts) => { return { id, + Renderer, query, variables, selector diff --git a/src/ui/scripts/loaders/sizesLoader.js b/src/ui/scripts/loaders/sizesLoader.js index c93f21ef..befa8aab 100644 --- a/src/ui/scripts/loaders/sizesLoader.js +++ b/src/ui/scripts/loaders/sizesLoader.js @@ -1,3 +1,27 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import CardSizes from '../components/cards/CardSizes' +import enhanceSizes from '../enhancers/enhanceSizes' + +const Renderer = (props) => { + return h(CardSizes, { + headline: props.headline, + range: props.widget.variables.range, + sorting: props.widget.variables.sorting, + stale: props.stale, + items: enhanceSizes(props.widget.value), + onMore: props.onMore + }) +} + +Renderer.propTypes = { + headline: PropTypes.string.isRequired, + widget: PropTypes.object.isRequired, + stale: PropTypes.bool.isRequired, + onMore: PropTypes.func +} + export default (domainId, opts) => { // TODO: Improve ids @@ -29,6 +53,7 @@ export default (domainId, opts) => { return { id, + Renderer, query, variables, selector diff --git a/src/ui/scripts/loaders/systemsLoader.js b/src/ui/scripts/loaders/systemsLoader.js index 19d5506b..850f2385 100644 --- a/src/ui/scripts/loaders/systemsLoader.js +++ b/src/ui/scripts/loaders/systemsLoader.js @@ -1,3 +1,27 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import CardSystems from '../components/cards/CardSystems' +import enhanceSystems from '../enhancers/enhanceSystems' + +const Renderer = (props) => { + return h(CardSystems, { + headline: props.headline, + range: props.widget.variables.range, + sorting: props.widget.variables.sorting, + stale: props.stale, + items: enhanceSystems(props.widget.value), + onMore: props.onMore + }) +} + +Renderer.propTypes = { + headline: PropTypes.string.isRequired, + widget: PropTypes.object.isRequired, + stale: PropTypes.bool.isRequired, + onMore: PropTypes.func +} + export default (domainId, opts) => { // TODO: Improve ids @@ -29,6 +53,7 @@ export default (domainId, opts) => { return { id, + Renderer, query, variables, selector diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js index e2fe5970..e5a720ca 100644 --- a/src/ui/scripts/loaders/viewsLoader.js +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -1,3 +1,27 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import CardViews from '../components/cards/CardViews' +import enhanceViews from '../enhancers/enhanceViews' + +const Renderer = (props) => { + return h(CardViews, { + headline: props.headline, + interval: props.widget.variables.interval, + sorting: props.widget.variables.sorting, + stale: props.stale, + items: enhanceViews(props.widget.value, 7), + onMore: props.onMore + }) +} + +Renderer.propTypes = { + headline: PropTypes.string.isRequired, + widget: PropTypes.object.isRequired, + stale: PropTypes.bool.isRequired, + onMore: PropTypes.func +} + export default (domainId, opts) => { // TODO: Improve ids @@ -27,6 +51,7 @@ export default (domainId, opts) => { return { id, + Renderer, query, variables, selector diff --git a/src/ui/scripts/reducers/widgets.js b/src/ui/scripts/reducers/widgets.js index 7be62c2d..f47092ad 100644 --- a/src/ui/scripts/reducers/widgets.js +++ b/src/ui/scripts/reducers/widgets.js @@ -13,6 +13,7 @@ export const initialState = () => ({ export const initialSubState = () => ({ value: [], + Renderer: () => null, variables: {}, fetching: false, error: undefined @@ -27,6 +28,7 @@ export default produce((draft, action) => { switch (action.type) { case SET_WIDGETS_START: + draft.value[action.id].Renderer = action.Renderer || initialSubState().Renderer draft.value[action.id].variables = action.variables || initialSubState().variables draft.value[action.id].fetching = true break diff --git a/src/ui/scripts/utils/useWidgets.js b/src/ui/scripts/utils/useWidgets.js index 77030512..63933937 100644 --- a/src/ui/scripts/utils/useWidgets.js +++ b/src/ui/scripts/utils/useWidgets.js @@ -1,6 +1,10 @@ -import { useEffect, useState } from 'react' +import { createElement as h, useEffect, useState } from 'react' + +import CardWidget from '../components/cards/CardWidget' import { initialSubState } from '../reducers/widgets' +import * as selectDomainsValue from '../selectors/selectDomainsValue' +import overviewRoute from './overviewRoute' export default (props, createLoader, opts) => { @@ -21,9 +25,27 @@ export default (props, createLoader, opts) => { }, [ props.domains.value, ...Object.values(opts) ]) - return widgetIds.map((widgetId) => { + const rawWidgets = widgetIds.map((widgetId) => { const widget = props.widgets.value[widgetId] return widget == null ? initialSubState() : widget }) + const renderedWidgets = rawWidgets.map( + (widgetData) => { + const domain = selectDomainsValue.byId(props, widgetData.variables.domainId) + + return h(CardWidget, { + key: domain.id, + headline: domain.title, + widget: widgetData, + onMore: () => props.setRoute(overviewRoute(domain)) + }) + } + ) + + return { + rawWidgets, + renderedWidgets + } + } \ No newline at end of file From 2c1a280ac39db99c7ea1652fc81bd7644dbfa671 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 18:04:19 +0100 Subject: [PATCH 047/208] Syntax adjustment --- src/ui/scripts/components/cards/CardDevices.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/scripts/components/cards/CardDevices.js b/src/ui/scripts/components/cards/CardDevices.js index 3e13ee69..20c34a96 100644 --- a/src/ui/scripts/components/cards/CardDevices.js +++ b/src/ui/scripts/components/cards/CardDevices.js @@ -18,6 +18,7 @@ const textLabel = (item, range, isRecent, isStale) => { if (isRecent) return 'Recent' return rangeLabel(range) + } const CardDevices = (props) => { From b47fc552d5a1ba2e065adeab333c8b9775f734c0 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 18:13:41 +0100 Subject: [PATCH 048/208] One card component for all list cards --- .../scripts/components/cards/CardBrowsers.js | 81 ------------------- .../scripts/components/cards/CardDevices.js | 81 ------------------- .../scripts/components/cards/CardLanguages.js | 81 ------------------- .../cards/{CardSystems.js => CardList.js} | 2 +- src/ui/scripts/components/cards/CardPages.js | 81 ------------------- src/ui/scripts/components/cards/CardSizes.js | 81 ------------------- .../components/routes/RouteOverview.js | 19 ++--- src/ui/scripts/loaders/browsersLoader.js | 4 +- src/ui/scripts/loaders/devicesLoader.js | 4 +- src/ui/scripts/loaders/languagesLoader.js | 4 +- src/ui/scripts/loaders/pagesLoader.js | 4 +- src/ui/scripts/loaders/sizesLoader.js | 4 +- src/ui/scripts/loaders/systemsLoader.js | 4 +- 13 files changed, 20 insertions(+), 430 deletions(-) delete mode 100644 src/ui/scripts/components/cards/CardBrowsers.js delete mode 100644 src/ui/scripts/components/cards/CardDevices.js delete mode 100644 src/ui/scripts/components/cards/CardLanguages.js rename src/ui/scripts/components/cards/{CardSystems.js => CardList.js} (96%) delete mode 100644 src/ui/scripts/components/cards/CardPages.js delete mode 100644 src/ui/scripts/components/cards/CardSizes.js diff --git a/src/ui/scripts/components/cards/CardBrowsers.js b/src/ui/scripts/components/cards/CardBrowsers.js deleted file mode 100644 index 9c55488a..00000000 --- a/src/ui/scripts/components/cards/CardBrowsers.js +++ /dev/null @@ -1,81 +0,0 @@ -import { createElement as h, useState } from 'react' -import PropTypes from 'prop-types' - -import { SORTINGS_TOP, SORTINGS_RECENT } from '../../../../constants/sortings' - -import Headline from '../Headline' -import Text from '../Text' -import Updating from '../Updating' -import PresentationCounterList from '../presentations/PresentationCounterList' -import PresentationList from '../presentations/PresentationList' -import relativeDate from '../../utils/relativeDate' -import rangeLabel from '../../utils/rangeLabel' - -const textLabel = (item, range, isRecent, isStale) => { - - if (isStale === true) return h(Updating) - if (item && item.date) return relativeDate(item.date) - if (isRecent) return 'Recent' - - return rangeLabel(range) - -} - -const CardBrowsers = (props) => { - - // Index of the active element - const [ active, setActive ] = useState() - - const onEnter = (index) => setActive(index) - const onLeave = () => setActive() - - const presentation = (() => { - - if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { - items: props.items - }) - - if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { - items: props.items, - onEnter, - onLeave - }) - - })() - - return ( - h('div', { - className: 'card' - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, textLabel( - props.items[active], - props.range, - props.sorting === SORTINGS_RECENT, - props.stale - )), - presentation - ) - ) - ) - -} - -CardBrowsers.propTypes = { - headline: PropTypes.string.isRequired, - range: PropTypes.string.isRequired, - sorting: PropTypes.string.isRequired, - stale: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - onMore: PropTypes.func -} - -export default CardBrowsers \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardDevices.js b/src/ui/scripts/components/cards/CardDevices.js deleted file mode 100644 index 20c34a96..00000000 --- a/src/ui/scripts/components/cards/CardDevices.js +++ /dev/null @@ -1,81 +0,0 @@ -import { createElement as h, useState } from 'react' -import PropTypes from 'prop-types' - -import { SORTINGS_TOP, SORTINGS_RECENT } from '../../../../constants/sortings' - -import Headline from '../Headline' -import Text from '../Text' -import Updating from '../Updating' -import PresentationCounterList from '../presentations/PresentationCounterList' -import PresentationList from '../presentations/PresentationList' -import relativeDate from '../../utils/relativeDate' -import rangeLabel from '../../utils/rangeLabel' - -const textLabel = (item, range, isRecent, isStale) => { - - if (isStale === true) return h(Updating) - if (item && item.date) return relativeDate(item.date) - if (isRecent) return 'Recent' - - return rangeLabel(range) - -} - -const CardDevices = (props) => { - - // Index of the active element - const [ active, setActive ] = useState() - - const onEnter = (index) => setActive(index) - const onLeave = () => setActive() - - const presentation = (() => { - - if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { - items: props.items - }) - - if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { - items: props.items, - onEnter, - onLeave - }) - - })() - - return ( - h('div', { - className: 'card' - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, textLabel( - props.items[active], - props.range, - props.sorting === SORTINGS_RECENT, - props.stale - )), - presentation - ) - ) - ) - -} - -CardDevices.propTypes = { - headline: PropTypes.string.isRequired, - range: PropTypes.string.isRequired, - sorting: PropTypes.string.isRequired, - stale: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - onMore: PropTypes.func -} - -export default CardDevices \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardLanguages.js b/src/ui/scripts/components/cards/CardLanguages.js deleted file mode 100644 index 8044b519..00000000 --- a/src/ui/scripts/components/cards/CardLanguages.js +++ /dev/null @@ -1,81 +0,0 @@ -import { createElement as h, useState } from 'react' -import PropTypes from 'prop-types' - -import { SORTINGS_TOP, SORTINGS_RECENT } from '../../../../constants/sortings' - -import Headline from '../Headline' -import Text from '../Text' -import Updating from '../Updating' -import PresentationCounterList from '../presentations/PresentationCounterList' -import PresentationList from '../presentations/PresentationList' -import relativeDate from '../../utils/relativeDate' -import rangeLabel from '../../utils/rangeLabel' - -const textLabel = (item, range, isRecent, isStale) => { - - if (isStale === true) return h(Updating) - if (item && item.date) return relativeDate(item.date) - if (isRecent) return 'Recent' - - return rangeLabel(range) - -} - -const CardLanguages = (props) => { - - // Index of the active element - const [ active, setActive ] = useState() - - const onEnter = (index) => setActive(index) - const onLeave = () => setActive() - - const presentation = (() => { - - if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { - items: props.items - }) - - if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { - items: props.items, - onEnter, - onLeave - }) - - })() - - return ( - h('div', { - className: 'card' - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, textLabel( - props.items[active], - props.range, - props.sorting === SORTINGS_RECENT, - props.stale - )), - presentation - ) - ) - ) - -} - -CardLanguages.propTypes = { - headline: PropTypes.string.isRequired, - range: PropTypes.string.isRequired, - sorting: PropTypes.string.isRequired, - stale: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - onMore: PropTypes.func -} - -export default CardLanguages \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardSystems.js b/src/ui/scripts/components/cards/CardList.js similarity index 96% rename from src/ui/scripts/components/cards/CardSystems.js rename to src/ui/scripts/components/cards/CardList.js index d36c70fe..2939de85 100644 --- a/src/ui/scripts/components/cards/CardSystems.js +++ b/src/ui/scripts/components/cards/CardList.js @@ -35,7 +35,7 @@ const CardSystems = (props) => { items: props.items }) - if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { + return h(PresentationList, { items: props.items, onEnter, onLeave diff --git a/src/ui/scripts/components/cards/CardPages.js b/src/ui/scripts/components/cards/CardPages.js deleted file mode 100644 index ecccf18d..00000000 --- a/src/ui/scripts/components/cards/CardPages.js +++ /dev/null @@ -1,81 +0,0 @@ -import { createElement as h, useState } from 'react' -import PropTypes from 'prop-types' - -import { SORTINGS_TOP, SORTINGS_RECENT } from '../../../../constants/sortings' - -import Headline from '../Headline' -import Text from '../Text' -import Updating from '../Updating' -import PresentationCounterList from '../presentations/PresentationCounterList' -import PresentationList from '../presentations/PresentationList' -import relativeDate from '../../utils/relativeDate' -import rangeLabel from '../../utils/rangeLabel' - -const textLabel = (item, range, isRecent, isStale) => { - - if (isStale === true) return h(Updating) - if (item && item.date) return relativeDate(item.date) - if (isRecent) return 'Recent' - - return rangeLabel(range) - -} - -const CardPages = (props) => { - - // Index of the active element - const [ active, setActive ] = useState() - - const onEnter = (index) => setActive(index) - const onLeave = () => setActive() - - const presentation = (() => { - - if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { - items: props.items - }) - - if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { - items: props.items, - onEnter, - onLeave - }) - - })() - - return ( - h('div', { - className: 'card' - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, textLabel( - props.items[active], - props.range, - props.sorting === SORTINGS_RECENT, - props.stale - )), - presentation - ) - ) - ) - -} - -CardPages.propTypes = { - headline: PropTypes.string.isRequired, - range: PropTypes.string.isRequired, - sorting: PropTypes.string.isRequired, - stale: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - onMore: PropTypes.func -} - -export default CardPages \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardSizes.js b/src/ui/scripts/components/cards/CardSizes.js deleted file mode 100644 index 9828515e..00000000 --- a/src/ui/scripts/components/cards/CardSizes.js +++ /dev/null @@ -1,81 +0,0 @@ -import { createElement as h, useState } from 'react' -import PropTypes from 'prop-types' - -import { SORTINGS_TOP, SORTINGS_RECENT } from '../../../../constants/sortings' - -import Headline from '../Headline' -import Text from '../Text' -import Updating from '../Updating' -import PresentationCounterList from '../presentations/PresentationCounterList' -import PresentationList from '../presentations/PresentationList' -import relativeDate from '../../utils/relativeDate' -import rangeLabel from '../../utils/rangeLabel' - -const textLabel = (item, range, isRecent, isStale) => { - - if (isStale === true) return h(Updating) - if (item && item.date) return relativeDate(item.date) - if (isRecent) return 'Recent' - - return rangeLabel(range) - -} - -const CardSizes = (props) => { - - // Index of the active element - const [ active, setActive ] = useState() - - const onEnter = (index) => setActive(index) - const onLeave = () => setActive() - - const presentation = (() => { - - if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { - items: props.items - }) - - if (props.sorting === SORTINGS_RECENT) return h(PresentationList, { - items: props.items, - onEnter, - onLeave - }) - - })() - - return ( - h('div', { - className: 'card' - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, textLabel( - props.items[active], - props.range, - props.sorting === SORTINGS_RECENT, - props.stale - )), - presentation - ) - ) - ) - -} - -CardSizes.propTypes = { - headline: PropTypes.string.isRequired, - range: PropTypes.string.isRequired, - sorting: PropTypes.string.isRequired, - stale: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - onMore: PropTypes.func -} - -export default CardSizes \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteOverview.js b/src/ui/scripts/components/routes/RouteOverview.js index c17832d8..b0273774 100644 --- a/src/ui/scripts/components/routes/RouteOverview.js +++ b/src/ui/scripts/components/routes/RouteOverview.js @@ -21,14 +21,9 @@ import enhanceLanguages from '../../enhancers/enhanceLanguages' import CardFacts from '../cards/CardFacts' import CardViews from '../cards/CardViews' -import CardPages from '../cards/CardPages' import CardReferrers from '../cards/CardReferrers' import CardDurations from '../cards/CardDurations' -import CardSystems from '../cards/CardSystems' -import CardDevices from '../cards/CardDevices' -import CardBrowsers from '../cards/CardBrowsers' -import CardSizes from '../cards/CardSizes' -import CardLanguages from '../cards/CardLanguages' +import CardList from '../cards/CardList' const RouteOverview = (props) => { @@ -69,7 +64,7 @@ const RouteOverview = (props) => { onMore: () => props.setRoute(route.ROUTE_DURATIONS) }), - h(CardPages, { + h(CardList, { headline: 'Pages', range: RANGES_LAST_24_HOURS, sorting: SORTINGS_TOP, @@ -89,7 +84,7 @@ const RouteOverview = (props) => { h('div', { className: 'content__spacer' }), - h(CardSystems, { + h(CardList, { headline: 'Systems', range: RANGES_LAST_24_HOURS, sorting: SORTINGS_TOP, @@ -98,7 +93,7 @@ const RouteOverview = (props) => { onMore: () => props.setRoute(route.ROUTE_SYSTEMS) }), - h(CardDevices, { + h(CardList, { headline: 'Devices', range: RANGES_LAST_24_HOURS, sorting: SORTINGS_TOP, @@ -107,7 +102,7 @@ const RouteOverview = (props) => { onMore: () => props.setRoute(route.ROUTE_DEVICES) }), - h(CardBrowsers, { + h(CardList, { headline: 'Browsers', range: RANGES_LAST_24_HOURS, sorting: SORTINGS_TOP, @@ -116,7 +111,7 @@ const RouteOverview = (props) => { onMore: () => props.setRoute(route.ROUTE_BROWSERS) }), - h(CardSizes, { + h(CardList, { headline: 'Sizes', range: RANGES_LAST_24_HOURS, sorting: SORTINGS_TOP, @@ -125,7 +120,7 @@ const RouteOverview = (props) => { onMore: () => props.setRoute(route.ROUTE_SIZES) }), - h(CardLanguages, { + h(CardList, { headline: 'Languages', range: RANGES_LAST_24_HOURS, sorting: SORTINGS_TOP, diff --git a/src/ui/scripts/loaders/browsersLoader.js b/src/ui/scripts/loaders/browsersLoader.js index d2558489..7238a303 100644 --- a/src/ui/scripts/loaders/browsersLoader.js +++ b/src/ui/scripts/loaders/browsersLoader.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import PropTypes from 'prop-types' -import CardBrowsers from '../components/cards/CardBrowsers' +import CardList from '../components/cards/CardList' import enhanceBrowsers from '../enhancers/enhanceBrowsers' const Renderer = (props) => { - return h(CardBrowsers, { + return h(CardList, { headline: props.headline, range: props.widget.variables.range, sorting: props.widget.variables.sorting, diff --git a/src/ui/scripts/loaders/devicesLoader.js b/src/ui/scripts/loaders/devicesLoader.js index 1bc06a78..c68bc7dc 100644 --- a/src/ui/scripts/loaders/devicesLoader.js +++ b/src/ui/scripts/loaders/devicesLoader.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import PropTypes from 'prop-types' -import CardDevices from '../components/cards/CardDevices' +import CardList from '../components/cards/CardList' import enhanceDevices from '../enhancers/enhanceDevices' const Renderer = (props) => { - return h(CardDevices, { + return h(CardList, { headline: props.headline, range: props.widget.variables.range, sorting: props.widget.variables.sorting, diff --git a/src/ui/scripts/loaders/languagesLoader.js b/src/ui/scripts/loaders/languagesLoader.js index 7f7aae9e..8b8fbacd 100644 --- a/src/ui/scripts/loaders/languagesLoader.js +++ b/src/ui/scripts/loaders/languagesLoader.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import PropTypes from 'prop-types' -import CardLanguages from '../components/cards/CardLanguages' +import CardList from '../components/cards/CardList' import enhanceLanguages from '../enhancers/enhanceLanguages' const Renderer = (props) => { - return h(CardLanguages, { + return h(CardList, { headline: props.headline, range: props.widget.variables.range, sorting: props.widget.variables.sorting, diff --git a/src/ui/scripts/loaders/pagesLoader.js b/src/ui/scripts/loaders/pagesLoader.js index e0db25e2..3346dec7 100644 --- a/src/ui/scripts/loaders/pagesLoader.js +++ b/src/ui/scripts/loaders/pagesLoader.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import PropTypes from 'prop-types' -import CardPages from '../components/cards/CardPages' +import CardList from '../components/cards/CardList' import enhancePages from '../enhancers/enhancePages' const Renderer = (props) => { - return h(CardPages, { + return h(CardList, { headline: props.headline, range: props.widget.variables.range, sorting: props.widget.variables.sorting, diff --git a/src/ui/scripts/loaders/sizesLoader.js b/src/ui/scripts/loaders/sizesLoader.js index befa8aab..ca5097c3 100644 --- a/src/ui/scripts/loaders/sizesLoader.js +++ b/src/ui/scripts/loaders/sizesLoader.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import PropTypes from 'prop-types' -import CardSizes from '../components/cards/CardSizes' +import CardList from '../components/cards/CardList' import enhanceSizes from '../enhancers/enhanceSizes' const Renderer = (props) => { - return h(CardSizes, { + return h(CardList, { headline: props.headline, range: props.widget.variables.range, sorting: props.widget.variables.sorting, diff --git a/src/ui/scripts/loaders/systemsLoader.js b/src/ui/scripts/loaders/systemsLoader.js index 850f2385..769f6104 100644 --- a/src/ui/scripts/loaders/systemsLoader.js +++ b/src/ui/scripts/loaders/systemsLoader.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import PropTypes from 'prop-types' -import CardSystems from '../components/cards/CardSystems' +import CardList from '../components/cards/CardList' import enhanceSystems from '../enhancers/enhanceSystems' const Renderer = (props) => { - return h(CardSystems, { + return h(CardList, { headline: props.headline, range: props.widget.variables.range, sorting: props.widget.variables.sorting, From 54f5ca576f5342cd72e6e361ebd4aac82ee87b17 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 18:43:36 +0100 Subject: [PATCH 049/208] Fix name --- src/ui/scripts/components/cards/CardList.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/scripts/components/cards/CardList.js b/src/ui/scripts/components/cards/CardList.js index 2939de85..af2598c6 100644 --- a/src/ui/scripts/components/cards/CardList.js +++ b/src/ui/scripts/components/cards/CardList.js @@ -21,7 +21,7 @@ const textLabel = (item, range, isRecent, isStale) => { } -const CardSystems = (props) => { +const CardList = (props) => { // Index of the active element const [ active, setActive ] = useState() @@ -69,7 +69,7 @@ const CardSystems = (props) => { } -CardSystems.propTypes = { +CardList.propTypes = { headline: PropTypes.string.isRequired, range: PropTypes.string.isRequired, sorting: PropTypes.string.isRequired, @@ -78,4 +78,4 @@ CardSystems.propTypes = { onMore: PropTypes.func } -export default CardSystems \ No newline at end of file +export default CardList \ No newline at end of file From 16ed1831b6bfb638df3e5664de3d2e5ba84d6e51 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 19:22:14 +0100 Subject: [PATCH 050/208] Reset events card for later --- .../scripts/components/routes/RouteEvents.js | 38 ++----------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/src/ui/scripts/components/routes/RouteEvents.js b/src/ui/scripts/components/routes/RouteEvents.js index 5c6ea579..d4d9d0da 100644 --- a/src/ui/scripts/components/routes/RouteEvents.js +++ b/src/ui/scripts/components/routes/RouteEvents.js @@ -1,40 +1,8 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h } from 'react' -// import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' -// import selectViewsValue from '../../selectors/selectViewsValue' -// import enhanceViews from '../../enhancers/enhanceViews' -// import mergeViews from '../../utils/mergeViews' -// import overviewRoute from '../../utils/overviewRoute' +const RouteEvents = () => { -import CardViews from '../cards/CardViews' - -const RouteEvents = (props) => { - - useEffect(() => { - - props.fetchEvents(props) - - }, []) - - return ( - h(Fragment, {}, - - props.events.value.map( - (event) => ( - h(CardViews, { - key: event.id, - headline: event.title, - interval: props.filter.interval, - // loading: props.views.fetching, - loading: false, - // items: enhanceViews(selectViewsValue(props, domain.id).value, 7), - items: [] - }) - ) - ) - - ) - ) + return null } From 790e6cd799f8d9585c14fe2139da2d8550c56613 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 19:23:39 +0100 Subject: [PATCH 051/208] CardChart instead of CardViews and CardDurations --- .../cards/{CardViews.js => CardChart.js} | 17 ++-- .../scripts/components/cards/CardDurations.js | 92 ------------------- .../components/routes/RouteDurations.js | 10 +- .../components/routes/RouteOverview.js | 11 ++- .../scripts/components/routes/RouteViews.js | 10 +- src/ui/scripts/loaders/durationsLoader.js | 6 +- src/ui/scripts/loaders/viewsLoader.js | 6 +- 7 files changed, 33 insertions(+), 119 deletions(-) rename src/ui/scripts/components/cards/{CardViews.js => CardChart.js} (86%) delete mode 100644 src/ui/scripts/components/cards/CardDurations.js diff --git a/src/ui/scripts/components/cards/CardViews.js b/src/ui/scripts/components/cards/CardChart.js similarity index 86% rename from src/ui/scripts/components/cards/CardViews.js rename to src/ui/scripts/components/cards/CardChart.js index c8935824..4351076b 100644 --- a/src/ui/scripts/components/cards/CardViews.js +++ b/src/ui/scripts/components/cards/CardChart.js @@ -11,8 +11,6 @@ import PresentationBarChart from '../presentations/PresentationBarChart' import relativeDays from '../../utils/relativeDays' import relativeMonths from '../../utils/relativeMonths' import relativeYears from '../../utils/relativeYears' -import formatNumber from '../../utils/formatNumber' -import status from '../../utils/status' const relativeFn = (interval) => { @@ -32,7 +30,7 @@ const textLabel = (active, interval, isStale) => { } -const CardViews = (props) => { +const CardChart = (props) => { // Index of the active element const [ active, setActive ] = useState(0) @@ -40,10 +38,6 @@ const CardViews = (props) => { const onEnter = (index) => setActive(index) const onLeave = () => setActive(0) - const { - isStale - } = status(props.items, props.loading) - return ( h('div', { className: classNames({ @@ -63,11 +57,11 @@ const CardViews = (props) => { }, textLabel( active, props.interval, - isStale + props.stale )), h(PresentationBarChart, { items: props.items, - formatter: formatNumber, + formatter: props.formatter, active: active, onEnter, onLeave @@ -78,13 +72,14 @@ const CardViews = (props) => { } -CardViews.propTypes = { +CardChart.propTypes = { wide: PropTypes.bool, headline: PropTypes.string.isRequired, interval: PropTypes.string.isRequired, loading: PropTypes.bool.isRequired, + stale: PropTypes.bool.isRequired, items: PropTypes.array.isRequired, onMore: PropTypes.func } -export default CardViews \ No newline at end of file +export default CardChart \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardDurations.js b/src/ui/scripts/components/cards/CardDurations.js deleted file mode 100644 index 2b096094..00000000 --- a/src/ui/scripts/components/cards/CardDurations.js +++ /dev/null @@ -1,92 +0,0 @@ -import { createElement as h, useState } from 'react' -import PropTypes from 'prop-types' -import classNames from 'classnames' - -import { INTERVALS_DAILY, INTERVALS_MONTHLY, INTERVALS_YEARLY } from '../../../../constants/intervals' - -import Headline from '../Headline' -import Text from '../Text' -import Updating from '../Updating' -import PresentationBarChart from '../presentations/PresentationBarChart' -import relativeDays from '../../utils/relativeDays' -import relativeMonths from '../../utils/relativeMonths' -import relativeYears from '../../utils/relativeYears' -import formatDuration from '../../utils/formatDuration' -import status from '../../utils/status' - -const formatter = (ms) => formatDuration(ms).toString() - -const relativeFn = (interval) => { - - switch (interval) { - case INTERVALS_DAILY: return relativeDays - case INTERVALS_MONTHLY: return relativeMonths - case INTERVALS_YEARLY: return relativeYears - } - -} - -const textLabel = (active, interval, isStale) => { - - if (isStale === true) return h(Updating) - - return relativeFn(interval)(active) - -} - -const CardDurations = (props) => { - - // Index of the active element - const [ active, setActive ] = useState(0) - - const onEnter = (index) => setActive(index) - const onLeave = () => setActive(0) - - const { - isStale - } = status(props.items, props.loading) - - return ( - h('div', { - className: classNames({ - 'card': true, - 'card--wide': props.wide === true - }) - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, textLabel( - active, - props.interval, - isStale - )), - h(PresentationBarChart, { - items: props.items, - formatter, - active: active, - onEnter, - onLeave - }) - ) - ) - ) - -} - -CardDurations.propTypes = { - wide: PropTypes.bool, - headline: PropTypes.string.isRequired, - interval: PropTypes.string.isRequired, - loading: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - onMore: PropTypes.func -} - -export default CardDurations \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index 68777797..2e960b40 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -1,10 +1,11 @@ import { createElement as h, Fragment } from 'react' +import CardChart from '../cards/CardChart' + import durationsLoader from '../../loaders/durationsLoader' import mergeDurations from '../../utils/mergeDurations' import useWidgets from '../../utils/useWidgets' - -import CardDurations from '../cards/CardDurations' +import formatDuration from '../../utils/formatDuration' const RouteDurations = (props) => { @@ -15,12 +16,13 @@ const RouteDurations = (props) => { return ( h(Fragment, {}, - h(CardDurations, { + h(CardChart, { wide: true, headline: 'Durations', interval: props.filter.interval, loading: props.fetching, - items: mergeDurations(rawWidgets) + items: mergeDurations(rawWidgets), + formatter: (ms) => formatDuration(ms).toString() }), renderedWidgets diff --git a/src/ui/scripts/components/routes/RouteOverview.js b/src/ui/scripts/components/routes/RouteOverview.js index b0273774..4ad623db 100644 --- a/src/ui/scripts/components/routes/RouteOverview.js +++ b/src/ui/scripts/components/routes/RouteOverview.js @@ -7,6 +7,8 @@ import { INTERVALS_DAILY } from '../../../../constants/intervals' import * as route from '../../constants/route' import { ALL_DOMAINS } from '../../actions/overview' import * as selectOverviewValue from '../../selectors/selectOverviewValue' +import formatNumber from '../../utils/formatNumber' +import formatDuration from '../../utils/formatDuration' import enhanceFacts from '../../enhancers/enhanceFacts' import enhanceViews from '../../enhancers/enhanceViews' @@ -20,9 +22,8 @@ import enhanceSizes from '../../enhancers/enhanceSizes' import enhanceLanguages from '../../enhancers/enhanceLanguages' import CardFacts from '../cards/CardFacts' -import CardViews from '../cards/CardViews' +import CardChart from '../cards/CardChart' import CardReferrers from '../cards/CardReferrers' -import CardDurations from '../cards/CardDurations' import CardList from '../cards/CardList' const RouteOverview = (props) => { @@ -46,21 +47,23 @@ const RouteOverview = (props) => { h('div', { className: 'content__spacer' }), - h(CardViews, { + h(CardChart, { wide: true, headline: 'Views', interval: INTERVALS_DAILY, loading: isLoading, items: enhanceViews(selectOverviewValue.withType(props, domainId, 'views'), 14), + formatter: formatNumber, onMore: () => props.setRoute(route.ROUTE_VIEWS) }), - h(CardDurations, { + h(CardChart, { wide: true, headline: 'Durations', interval: INTERVALS_DAILY, loading: isLoading, items: enhanceDurations(selectOverviewValue.withType(props, domainId, 'durations'), 14), + formatter: (ms) => formatDuration(ms).toString(), onMore: () => props.setRoute(route.ROUTE_DURATIONS) }), diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 603bd5f8..185819c1 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -1,11 +1,12 @@ import { createElement as h, Fragment } from 'react' +import CardChart from '../cards/CardChart' + import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' import viewsLoader from '../../loaders/viewsLoader' import mergeViews from '../../utils/mergeViews' import useWidgets from '../../utils/useWidgets' - -import CardViews from '../cards/CardViews' +import formatNumber from '../../utils/formatNumber' const RouteViews = (props) => { @@ -16,7 +17,7 @@ const RouteViews = (props) => { return ( h(Fragment, {}, - h(CardViews, { + h(CardChart, { wide: true, headline: ({ [VIEWS_TYPE_UNIQUE]: 'Site Views', @@ -24,7 +25,8 @@ const RouteViews = (props) => { })[props.filter.viewsType], interval: props.filter.interval, loading: props.fetching, - items: mergeViews(rawWidgets) + items: mergeViews(rawWidgets), + formatter: formatNumber }), renderedWidgets diff --git a/src/ui/scripts/loaders/durationsLoader.js b/src/ui/scripts/loaders/durationsLoader.js index 2c28b155..1145653b 100644 --- a/src/ui/scripts/loaders/durationsLoader.js +++ b/src/ui/scripts/loaders/durationsLoader.js @@ -1,16 +1,18 @@ import { createElement as h } from 'react' import PropTypes from 'prop-types' -import CardDurations from '../components/cards/CardDurations' +import CardChart from '../components/cards/CardChart' import enhanceDurations from '../enhancers/enhanceDurations' +import formatDuration from '../utils/formatDuration' const Renderer = (props) => { - return h(CardDurations, { + return h(CardChart, { headline: props.headline, interval: props.widget.variables.interval, sorting: props.widget.variables.sorting, stale: props.stale, items: enhanceDurations(props.widget.value, 7), + formatter: (ms) => formatDuration(ms).toString(), onMore: props.onMore }) } diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js index e5a720ca..d264d991 100644 --- a/src/ui/scripts/loaders/viewsLoader.js +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -1,16 +1,18 @@ import { createElement as h } from 'react' import PropTypes from 'prop-types' -import CardViews from '../components/cards/CardViews' +import CardChart from '../components/cards/CardChart' import enhanceViews from '../enhancers/enhanceViews' +import formatNumber from '../utils/formatNumber' const Renderer = (props) => { - return h(CardViews, { + return h(CardChart, { headline: props.headline, interval: props.widget.variables.interval, sorting: props.widget.variables.sorting, stale: props.stale, items: enhanceViews(props.widget.value, 7), + formatter: formatNumber, onMore: props.onMore }) } From 75e3dc4f661e76badc0f878b84943526fab01620 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 19:24:48 +0100 Subject: [PATCH 052/208] Remove console.log --- src/ui/scripts/utils/mergeViews.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/scripts/utils/mergeViews.js b/src/ui/scripts/utils/mergeViews.js index 392684e5..66998be1 100644 --- a/src/ui/scripts/utils/mergeViews.js +++ b/src/ui/scripts/utils/mergeViews.js @@ -6,7 +6,6 @@ export default (widgets) => { // Enhance views for all widgets const enhancedViews = widgets.map((widget) => { - console.log(widget) return enhanceViews(widget.value, 14) }) From c3e55d2457721744fecd80e5389626d69d26bed5 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 19:26:32 +0100 Subject: [PATCH 053/208] Remove unused prop --- src/ui/scripts/components/cards/CardChart.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/scripts/components/cards/CardChart.js b/src/ui/scripts/components/cards/CardChart.js index 4351076b..fb2ef455 100644 --- a/src/ui/scripts/components/cards/CardChart.js +++ b/src/ui/scripts/components/cards/CardChart.js @@ -76,7 +76,6 @@ CardChart.propTypes = { wide: PropTypes.bool, headline: PropTypes.string.isRequired, interval: PropTypes.string.isRequired, - loading: PropTypes.bool.isRequired, stale: PropTypes.bool.isRequired, items: PropTypes.array.isRequired, onMore: PropTypes.func From 9392d2ac150c6048697906ccff7d403d862bb7e2 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 20:07:04 +0100 Subject: [PATCH 054/208] Remove unused prop --- src/ui/scripts/components/routes/RouteDurations.js | 1 - src/ui/scripts/components/routes/RouteViews.js | 1 - 2 files changed, 2 deletions(-) diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index 2e960b40..49ad9115 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -20,7 +20,6 @@ const RouteDurations = (props) => { wide: true, headline: 'Durations', interval: props.filter.interval, - loading: props.fetching, items: mergeDurations(rawWidgets), formatter: (ms) => formatDuration(ms).toString() }), diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 185819c1..bc835633 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -24,7 +24,6 @@ const RouteViews = (props) => { [VIEWS_TYPE_TOTAL]: 'Page Views' })[props.filter.viewsType], interval: props.filter.interval, - loading: props.fetching, items: mergeViews(rawWidgets), formatter: formatNumber }), From 5e8bc5ec92228591ed9a20042ffe1c0f4b62c3c4 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 29 Nov 2020 20:22:31 +0100 Subject: [PATCH 055/208] Reduce amount of set states --- src/ui/scripts/actions/permanentTokens.js | 31 ++++++++++------------ src/ui/scripts/reducers/permanentTokens.js | 12 ++++++--- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/ui/scripts/actions/permanentTokens.js b/src/ui/scripts/actions/permanentTokens.js index e3a9fcc5..1e557ec6 100644 --- a/src/ui/scripts/actions/permanentTokens.js +++ b/src/ui/scripts/actions/permanentTokens.js @@ -1,13 +1,18 @@ import api from '../utils/api' import signalHandler from '../utils/signalHandler' -export const SET_PERMANENT_TOKENS_VALUE = Symbol() +export const SET_PERMANENT_TOKENS_START = Symbol() +export const SET_PERMANENT_TOKENS_END = Symbol() export const SET_PERMANENT_TOKENS_FETCHING = Symbol() export const SET_PERMANENT_TOKENS_ERROR = Symbol() -export const setPermanentTokensValue = (payload) => ({ - type: SET_PERMANENT_TOKENS_VALUE, - payload +export const setPermanentTokensStart = () => ({ + type: SET_PERMANENT_TOKENS_START +}) + +export const setPermanentTokensEnd = (value) => ({ + type: SET_PERMANENT_TOKENS_END, + value }) export const setPermanentTokensFetching = (payload) => ({ @@ -22,8 +27,7 @@ export const setPermanentTokensError = (payload) => ({ export const fetchPermanentTokens = signalHandler((signal) => (props) => async (dispatch) => { - dispatch(setPermanentTokensFetching(true)) - dispatch(setPermanentTokensError()) + dispatch(setPermanentTokensStart()) try { @@ -40,8 +44,7 @@ export const fetchPermanentTokens = signalHandler((signal) => (props) => async ( signal: signal() }) - dispatch(setPermanentTokensValue(data.permanentTokens)) - dispatch(setPermanentTokensFetching(false)) + dispatch(setPermanentTokensEnd(data.permanentTokens)) } catch (err) { @@ -56,8 +59,7 @@ export const fetchPermanentTokens = signalHandler((signal) => (props) => async ( export const addPermanentToken = (props, state) => async (dispatch) => { - dispatch(setPermanentTokensFetching(true)) - dispatch(setPermanentTokensError()) + dispatch(setPermanentTokensStart()) try { @@ -78,7 +80,6 @@ export const addPermanentToken = (props, state) => async (dispatch) => { }) await dispatch(fetchPermanentTokens(props)) - dispatch(setPermanentTokensFetching(false)) } catch (err) { @@ -93,8 +94,7 @@ export const addPermanentToken = (props, state) => async (dispatch) => { export const updatePermanentToken = signalHandler((signal) => (props, permanentTokenId, state) => async (dispatch) => { - dispatch(setPermanentTokensFetching(true)) - dispatch(setPermanentTokensError()) + dispatch(setPermanentTokensStart()) try { @@ -117,7 +117,6 @@ export const updatePermanentToken = signalHandler((signal) => (props, permanentT }) await dispatch(fetchPermanentTokens(props)) - dispatch(setPermanentTokensFetching(false)) } catch (err) { @@ -132,8 +131,7 @@ export const updatePermanentToken = signalHandler((signal) => (props, permanentT export const deletePermanentToken = signalHandler((signal) => (props, permanentTokenId) => async (dispatch) => { - dispatch(setPermanentTokensFetching(true)) - dispatch(setPermanentTokensError()) + dispatch(setPermanentTokensStart()) try { @@ -153,7 +151,6 @@ export const deletePermanentToken = signalHandler((signal) => (props, permanentT }) await dispatch(fetchPermanentTokens(props)) - dispatch(setPermanentTokensFetching(false)) } catch (err) { diff --git a/src/ui/scripts/reducers/permanentTokens.js b/src/ui/scripts/reducers/permanentTokens.js index 0572d947..938d0aa4 100644 --- a/src/ui/scripts/reducers/permanentTokens.js +++ b/src/ui/scripts/reducers/permanentTokens.js @@ -1,7 +1,8 @@ import produce from 'immer' import { - SET_PERMANENT_TOKENS_VALUE, + SET_PERMANENT_TOKENS_START, + SET_PERMANENT_TOKENS_END, SET_PERMANENT_TOKENS_FETCHING, SET_PERMANENT_TOKENS_ERROR } from '../actions' @@ -15,8 +16,13 @@ export const initialState = () => ({ export default produce((draft, action) => { switch (action.type) { - case SET_PERMANENT_TOKENS_VALUE: - draft.value = action.payload || initialState().value + case SET_PERMANENT_TOKENS_START: + draft.fetching = true + draft.error = initialState().error + break + case SET_PERMANENT_TOKENS_END: + draft.value = action.value || initialState().value + draft.fetching = false break case SET_PERMANENT_TOKENS_FETCHING: draft.fetching = action.payload || initialState().fetching From 5999fc7f9584143c2ac0835c439b22e362784472 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 5 Dec 2020 18:09:23 +0100 Subject: [PATCH 056/208] Refactor rendering --- src/ui/scripts/actions/widgets.js | 4 +- src/ui/scripts/components/cards/CardChart.js | 84 ------- src/ui/scripts/components/cards/CardList.js | 81 ------- .../scripts/components/cards/CardReferrers.js | 80 ------- src/ui/scripts/components/cards/CardWidget.js | 94 ++++---- .../components/renderers/RendererChart.js | 56 +++++ .../components/renderers/RendererList.js | 51 +++++ .../components/renderers/RendererReferrers.js | 49 +++++ .../components/routes/RouteDurations.js | 23 +- .../components/routes/RouteOverview.js | 208 +++++++++--------- .../scripts/components/routes/RouteViews.js | 31 +-- src/ui/scripts/loaders/browsersLoader.js | 26 +-- src/ui/scripts/loaders/devicesLoader.js | 26 +-- src/ui/scripts/loaders/durationsLoader.js | 30 +-- src/ui/scripts/loaders/languagesLoader.js | 26 +-- src/ui/scripts/loaders/pagesLoader.js | 26 +-- src/ui/scripts/loaders/referrersLoader.js | 26 +-- src/ui/scripts/loaders/sizesLoader.js | 26 +-- src/ui/scripts/loaders/systemsLoader.js | 26 +-- src/ui/scripts/loaders/viewsLoader.js | 30 +-- 20 files changed, 373 insertions(+), 630 deletions(-) delete mode 100644 src/ui/scripts/components/cards/CardChart.js delete mode 100644 src/ui/scripts/components/cards/CardList.js delete mode 100644 src/ui/scripts/components/cards/CardReferrers.js create mode 100644 src/ui/scripts/components/renderers/RendererChart.js create mode 100644 src/ui/scripts/components/renderers/RendererList.js create mode 100644 src/ui/scripts/components/renderers/RendererReferrers.js diff --git a/src/ui/scripts/actions/widgets.js b/src/ui/scripts/actions/widgets.js index 838987e3..4f494fc7 100644 --- a/src/ui/scripts/actions/widgets.js +++ b/src/ui/scripts/actions/widgets.js @@ -33,7 +33,7 @@ export const setWidgetsError = (id, payload) => ({ export const fetchWidget = signalHandler((signal) => (props, loader) => async (dispatch) => { - const { id, Renderer, query, variables, selector } = loader + const { id, Renderer, query, variables, selector, enhancer } = loader dispatch(setWidgetsStart(id, Renderer, variables)) @@ -46,7 +46,7 @@ export const fetchWidget = signalHandler((signal) => (props, loader) => async (d signal: signal(id) }) - dispatch(setWidgetsEnd(id, selector(data))) + dispatch(setWidgetsEnd(id, enhancer(selector(data)))) } catch (err) { diff --git a/src/ui/scripts/components/cards/CardChart.js b/src/ui/scripts/components/cards/CardChart.js deleted file mode 100644 index fb2ef455..00000000 --- a/src/ui/scripts/components/cards/CardChart.js +++ /dev/null @@ -1,84 +0,0 @@ -import { createElement as h, useState } from 'react' -import PropTypes from 'prop-types' -import classNames from 'classnames' - -import { INTERVALS_DAILY, INTERVALS_MONTHLY, INTERVALS_YEARLY } from '../../../../constants/intervals' - -import Headline from '../Headline' -import Text from '../Text' -import Updating from '../Updating' -import PresentationBarChart from '../presentations/PresentationBarChart' -import relativeDays from '../../utils/relativeDays' -import relativeMonths from '../../utils/relativeMonths' -import relativeYears from '../../utils/relativeYears' - -const relativeFn = (interval) => { - - switch (interval) { - case INTERVALS_DAILY: return relativeDays - case INTERVALS_MONTHLY: return relativeMonths - case INTERVALS_YEARLY: return relativeYears - } - -} - -const textLabel = (active, interval, isStale) => { - - if (isStale === true) return h(Updating) - - return relativeFn(interval)(active) - -} - -const CardChart = (props) => { - - // Index of the active element - const [ active, setActive ] = useState(0) - - const onEnter = (index) => setActive(index) - const onLeave = () => setActive(0) - - return ( - h('div', { - className: classNames({ - 'card': true, - 'card--wide': props.wide === true - }) - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, textLabel( - active, - props.interval, - props.stale - )), - h(PresentationBarChart, { - items: props.items, - formatter: props.formatter, - active: active, - onEnter, - onLeave - }) - ) - ) - ) - -} - -CardChart.propTypes = { - wide: PropTypes.bool, - headline: PropTypes.string.isRequired, - interval: PropTypes.string.isRequired, - stale: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - onMore: PropTypes.func -} - -export default CardChart \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardList.js b/src/ui/scripts/components/cards/CardList.js deleted file mode 100644 index af2598c6..00000000 --- a/src/ui/scripts/components/cards/CardList.js +++ /dev/null @@ -1,81 +0,0 @@ -import { createElement as h, useState } from 'react' -import PropTypes from 'prop-types' - -import { SORTINGS_TOP, SORTINGS_RECENT } from '../../../../constants/sortings' - -import Headline from '../Headline' -import Text from '../Text' -import Updating from '../Updating' -import PresentationCounterList from '../presentations/PresentationCounterList' -import PresentationList from '../presentations/PresentationList' -import relativeDate from '../../utils/relativeDate' -import rangeLabel from '../../utils/rangeLabel' - -const textLabel = (item, range, isRecent, isStale) => { - - if (isStale === true) return h(Updating) - if (item && item.date) return relativeDate(item.date) - if (isRecent) return 'Recent' - - return rangeLabel(range) - -} - -const CardList = (props) => { - - // Index of the active element - const [ active, setActive ] = useState() - - const onEnter = (index) => setActive(index) - const onLeave = () => setActive() - - const presentation = (() => { - - if (props.sorting === SORTINGS_TOP) return h(PresentationCounterList, { - items: props.items - }) - - return h(PresentationList, { - items: props.items, - onEnter, - onLeave - }) - - })() - - return ( - h('div', { - className: 'card' - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, textLabel( - props.items[active], - props.range, - props.sorting === SORTINGS_RECENT, - props.stale - )), - presentation - ) - ) - ) - -} - -CardList.propTypes = { - headline: PropTypes.string.isRequired, - range: PropTypes.string.isRequired, - sorting: PropTypes.string.isRequired, - stale: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - onMore: PropTypes.func -} - -export default CardList \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardReferrers.js b/src/ui/scripts/components/cards/CardReferrers.js deleted file mode 100644 index 78456c5e..00000000 --- a/src/ui/scripts/components/cards/CardReferrers.js +++ /dev/null @@ -1,80 +0,0 @@ -import { createElement as h, useState } from 'react' -import PropTypes from 'prop-types' - -import { SORTINGS_NEW, SORTINGS_RECENT } from '../../../../constants/sortings' - -import Headline from '../Headline' -import Text from '../Text' -import Updating from '../Updating' -import PresentationIconList from '../presentations/PresentationIconList' -import relativeDate from '../../utils/relativeDate' -import rangeLabel from '../../utils/rangeLabel' - -const textLabel = (item, range, isRecent, isNew, isStale) => { - - if (isStale === true) return h(Updating) - - if (item && item.date) return relativeDate(item.date) - if (item && item.count) return `${ item.count } ${ item.count === 1 ? 'visit' : 'visits' }` - - if (isRecent) return 'Recent' - if (isNew) return 'New' - - return rangeLabel(range) - -} - -const CardReferrers = (props) => { - - // Index of the active element - const [ active, setActive ] = useState() - - const onEnter = (index) => setActive(index) - const onLeave = () => setActive() - - const presentation = (() => { - - return h(PresentationIconList, { - items: props.items, - onEnter, - onLeave - }) - - })() - - return ( - h('div', { - className: 'card' - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, textLabel( - props.items[active], - props.range, - props.sorting === SORTINGS_RECENT, - props.sorting === SORTINGS_NEW, - props.stale - )), - presentation - ) - ) - ) - -} - -CardReferrers.propTypes = { - headline: PropTypes.string.isRequired, - range: PropTypes.string.isRequired, - stale: PropTypes.bool.isRequired, - items: PropTypes.array.isRequired, - onMore: PropTypes.func -} - -export default CardReferrers \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardWidget.js b/src/ui/scripts/components/cards/CardWidget.js index 90146f22..13e60213 100644 --- a/src/ui/scripts/components/cards/CardWidget.js +++ b/src/ui/scripts/components/cards/CardWidget.js @@ -1,8 +1,10 @@ -import { createElement as h } from 'react' +import { createElement as h, useState } from 'react' import PropTypes from 'prop-types' +import classNames from 'classnames' import Headline from '../Headline' import Text from '../Text' +import Updating from '../Updating' import PresentationEmptyState, { ICON_LOADING, ICON_WARNING } from '../presentations/PresentationEmptyState' import status from '../../utils/status' @@ -14,69 +16,55 @@ const CardWidget = (props) => { isLoading } = status(props.widget.value, props.widget.fetching) - if (isLoading === true) { + const [ textLabel, setTextLabel ] = useState('') - return ( - h('div', { - className: 'card' - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, 'Test'), - h(PresentationEmptyState, { - icon: ICON_LOADING - }, 'Loading data') - ) - ) - ) + const presentation = (() => { + if (isLoading === true) { + return h(PresentationEmptyState, { + icon: ICON_LOADING + }, 'Loading data') + } - } + if (isEmpty === true) { + h(PresentationEmptyState, { + icon: ICON_WARNING + }, 'No data') + } - if (isEmpty === true) { + return h(props.widget.Renderer, { + widget: props.widget, + setTextLabel + }) + })() - return ( - h('div', { - className: 'card' - }, - h('div', { className: 'card__inner' }, - h(Headline, { - type: 'h2', - size: 'medium', - onClick: props.onMore - }, props.headline), - h(Text, { - type: 'div', - spacing: false - }, 'Test'), - h(PresentationEmptyState, { - icon: ICON_WARNING - }, 'No data') - ) + return ( + h('div', { + className: classNames({ + 'card': true, + 'card--wide': props.wide === true + }) + }, + h('div', { className: 'card__inner' }, + h(Headline, { + type: 'h2', + size: 'medium', + onClick: props.onMore + }, props.headline), + h(Text, { + type: 'div', + spacing: false + }, isStale === true ? h(Updating) : textLabel), + presentation ) ) - - } - - return h(props.widget.Renderer, { - headline: props.headline, - widget: props.widget, - stale: isStale, - onMore: props.onMore - }) + ) } CardWidget.propTypes = { + wide: PropTypes.bool, headline: PropTypes.string.isRequired, - widget: PropTypes.object.isRequired, - onMore: PropTypes.func + widget: PropTypes.object.isRequired } export default CardWidget \ No newline at end of file diff --git a/src/ui/scripts/components/renderers/RendererChart.js b/src/ui/scripts/components/renderers/RendererChart.js new file mode 100644 index 00000000..670e3007 --- /dev/null +++ b/src/ui/scripts/components/renderers/RendererChart.js @@ -0,0 +1,56 @@ +import { createElement as h, useState, useEffect } from 'react' +import PropTypes from 'prop-types' + +import { INTERVALS_DAILY, INTERVALS_MONTHLY, INTERVALS_YEARLY } from '../../../../constants/intervals' + +import PresentationBarChart from '../presentations/PresentationBarChart' +import relativeDays from '../../utils/relativeDays' +import relativeMonths from '../../utils/relativeMonths' +import relativeYears from '../../utils/relativeYears' + +const relativeFn = (interval) => { + + switch (interval) { + case INTERVALS_DAILY: return relativeDays + case INTERVALS_MONTHLY: return relativeMonths + case INTERVALS_YEARLY: return relativeYears + } + +} + +const textLabel = (active, interval) => { + + return relativeFn(interval)(active) + +} + +const RendererChart = (props) => { + + const items = props.widget.value + const { interval } = props.widget.variables + + // Index of the active element + const [ active, setActive ] = useState(0) + + const onEnter = (index) => setActive(index) + const onLeave = () => setActive(0) + + const label = textLabel(active, interval) + useEffect(() => props.setTextLabel(label), [ label ]) + + return h(PresentationBarChart, { + items, + formatter: props.formatter, + active: active, + onEnter, + onLeave + }) + +} + +RendererChart.propTypes = { + widget: PropTypes.object.isRequired, + setTextLabel: PropTypes.func.isRequired +} + +export default RendererChart \ No newline at end of file diff --git a/src/ui/scripts/components/renderers/RendererList.js b/src/ui/scripts/components/renderers/RendererList.js new file mode 100644 index 00000000..27b68fca --- /dev/null +++ b/src/ui/scripts/components/renderers/RendererList.js @@ -0,0 +1,51 @@ +import { createElement as h, useState, useEffect } from 'react' +import PropTypes from 'prop-types' + +import { SORTINGS_TOP, SORTINGS_RECENT } from '../../../../constants/sortings' + +import PresentationCounterList from '../presentations/PresentationCounterList' +import PresentationList from '../presentations/PresentationList' +import relativeDate from '../../utils/relativeDate' +import rangeLabel from '../../utils/rangeLabel' + +const textLabel = (item, range, isRecent) => { + + if (item && item.date) return relativeDate(item.date) + if (isRecent) return 'Recent' + + return rangeLabel(range) + +} + +const CardList = (props) => { + + const items = props.widget.value + const { range, sorting } = props.widget.variables + + // Index of the active element + const [ active, setActive ] = useState() + + const onEnter = (index) => setActive(index) + const onLeave = () => setActive() + + const label = textLabel(items[active], range, sorting === SORTINGS_RECENT) + useEffect(() => props.setTextLabel(label), [ label ]) + + if (sorting === SORTINGS_TOP) return h(PresentationCounterList, { + items + }) + + return h(PresentationList, { + items, + onEnter, + onLeave + }) + +} + +CardList.propTypes = { + widget: PropTypes.object.isRequired, + setTextLabel: PropTypes.func.isRequired +} + +export default CardList \ No newline at end of file diff --git a/src/ui/scripts/components/renderers/RendererReferrers.js b/src/ui/scripts/components/renderers/RendererReferrers.js new file mode 100644 index 00000000..bc85b421 --- /dev/null +++ b/src/ui/scripts/components/renderers/RendererReferrers.js @@ -0,0 +1,49 @@ +import { createElement as h, useState, useEffect } from 'react' +import PropTypes from 'prop-types' + +import { SORTINGS_NEW, SORTINGS_RECENT } from '../../../../constants/sortings' + +import PresentationIconList from '../presentations/PresentationIconList' +import relativeDate from '../../utils/relativeDate' +import rangeLabel from '../../utils/rangeLabel' + +const textLabel = (item, range, isRecent, isNew) => { + + if (item && item.date) return relativeDate(item.date) + if (item && item.count) return `${ item.count } ${ item.count === 1 ? 'visit' : 'visits' }` + + if (isRecent) return 'Recent' + if (isNew) return 'New' + + return rangeLabel(range) + +} + +const CardReferrers = (props) => { + + const items = props.widget.value + const { range, sorting } = props.widget.variables + + // Index of the active element + const [ active, setActive ] = useState() + + const onEnter = (index) => setActive(index) + const onLeave = () => setActive() + + const label = textLabel(items[active], range, sorting === SORTINGS_RECENT, sorting === SORTINGS_NEW) + useEffect(() => props.setTextLabel(label), [ label ]) + + return h(PresentationIconList, { + items, + onEnter, + onLeave + }) + +} + +CardReferrers.propTypes = { + widget: PropTypes.object.isRequired, + setTextLabel: PropTypes.func.isRequired +} + +export default CardReferrers \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index 49ad9115..b10fc720 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -1,28 +1,29 @@ import { createElement as h, Fragment } from 'react' -import CardChart from '../cards/CardChart' +// import CardChart from '../renderers/RendererChart' import durationsLoader from '../../loaders/durationsLoader' -import mergeDurations from '../../utils/mergeDurations' +// import mergeDurations from '../../utils/mergeDurations' import useWidgets from '../../utils/useWidgets' -import formatDuration from '../../utils/formatDuration' +// import formatDuration from '../../utils/formatDuration' const RouteDurations = (props) => { - const { rawWidgets, renderedWidgets } = useWidgets(props, durationsLoader, { + // const { rawWidgets, renderedWidgets } = useWidgets(props, durationsLoader, { + const { renderedWidgets } = useWidgets(props, durationsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) return ( h(Fragment, {}, - h(CardChart, { - wide: true, - headline: 'Durations', - interval: props.filter.interval, - items: mergeDurations(rawWidgets), - formatter: (ms) => formatDuration(ms).toString() - }), + // h(CardChart, { + // wide: true, + // headline: 'Durations', + // interval: props.filter.interval, + // items: mergeDurations(rawWidgets), + // formatter: (ms) => formatDuration(ms).toString() + // }), renderedWidgets ) diff --git a/src/ui/scripts/components/routes/RouteOverview.js b/src/ui/scripts/components/routes/RouteOverview.js index 4ad623db..5937c7e5 100644 --- a/src/ui/scripts/components/routes/RouteOverview.js +++ b/src/ui/scripts/components/routes/RouteOverview.js @@ -1,30 +1,30 @@ import { createElement as h, Fragment, useEffect } from 'react' -import { SORTINGS_TOP } from '../../../../constants/sortings' -import { RANGES_LAST_24_HOURS } from '../../../../constants/ranges' -import { INTERVALS_DAILY } from '../../../../constants/intervals' +// import { SORTINGS_TOP } from '../../../../constants/sortings' +// import { RANGES_LAST_24_HOURS } from '../../../../constants/ranges' +// import { INTERVALS_DAILY } from '../../../../constants/intervals' -import * as route from '../../constants/route' +// import * as route from '../../constants/route' import { ALL_DOMAINS } from '../../actions/overview' import * as selectOverviewValue from '../../selectors/selectOverviewValue' -import formatNumber from '../../utils/formatNumber' -import formatDuration from '../../utils/formatDuration' +// import formatNumber from '../../utils/formatNumber' +// import formatDuration from '../../utils/formatDuration' import enhanceFacts from '../../enhancers/enhanceFacts' -import enhanceViews from '../../enhancers/enhanceViews' -import enhancePages from '../../enhancers/enhancePages' -import enhanceReferrers from '../../enhancers/enhanceReferrers' -import enhanceDurations from '../../enhancers/enhanceDurations' -import enhanceSystems from '../../enhancers/enhanceSystems' -import enhanceDevices from '../../enhancers/enhanceDevices' -import enhanceBrowsers from '../../enhancers/enhanceBrowsers' -import enhanceSizes from '../../enhancers/enhanceSizes' -import enhanceLanguages from '../../enhancers/enhanceLanguages' +// import enhanceViews from '../../enhancers/enhanceViews' +// import enhancePages from '../../enhancers/enhancePages' +// import enhanceReferrers from '../../enhancers/enhanceReferrers' +// import enhanceDurations from '../../enhancers/enhanceDurations' +// import enhanceSystems from '../../enhancers/enhanceSystems' +// import enhanceDevices from '../../enhancers/enhanceDevices' +// import enhanceBrowsers from '../../enhancers/enhanceBrowsers' +// import enhanceSizes from '../../enhancers/enhanceSizes' +// import enhanceLanguages from '../../enhancers/enhanceLanguages' import CardFacts from '../cards/CardFacts' -import CardChart from '../cards/CardChart' -import CardReferrers from '../cards/CardReferrers' -import CardList from '../cards/CardList' +// import RendererChart from '../renderers/RendererChart' +// import RendererReferrers from '../renderers/RendererReferrers' +// import RendererList from '../renderers/RendererList' const RouteOverview = (props) => { @@ -45,92 +45,92 @@ const RouteOverview = (props) => { items: enhanceFacts(selectOverviewValue.withoutType(props, domainId).facts) }), - h('div', { className: 'content__spacer' }), - - h(CardChart, { - wide: true, - headline: 'Views', - interval: INTERVALS_DAILY, - loading: isLoading, - items: enhanceViews(selectOverviewValue.withType(props, domainId, 'views'), 14), - formatter: formatNumber, - onMore: () => props.setRoute(route.ROUTE_VIEWS) - }), - - h(CardChart, { - wide: true, - headline: 'Durations', - interval: INTERVALS_DAILY, - loading: isLoading, - items: enhanceDurations(selectOverviewValue.withType(props, domainId, 'durations'), 14), - formatter: (ms) => formatDuration(ms).toString(), - onMore: () => props.setRoute(route.ROUTE_DURATIONS) - }), - - h(CardList, { - headline: 'Pages', - range: RANGES_LAST_24_HOURS, - sorting: SORTINGS_TOP, - loading: isLoading, - items: enhancePages(selectOverviewValue.withType(props, domainId, 'pages')), - onMore: () => props.setRoute(route.ROUTE_PAGES) - }), - - h(CardReferrers, { - headline: 'Referrers', - range: RANGES_LAST_24_HOURS, - sorting: SORTINGS_TOP, - loading: isLoading, - items: enhanceReferrers(selectOverviewValue.withType(props, domainId, 'referrers')), - onMore: () => props.setRoute(route.ROUTE_REFERRERS) - }), - - h('div', { className: 'content__spacer' }), - - h(CardList, { - headline: 'Systems', - range: RANGES_LAST_24_HOURS, - sorting: SORTINGS_TOP, - loading: isLoading, - items: enhanceSystems(selectOverviewValue.withType(props, domainId, 'systems')), - onMore: () => props.setRoute(route.ROUTE_SYSTEMS) - }), - - h(CardList, { - headline: 'Devices', - range: RANGES_LAST_24_HOURS, - sorting: SORTINGS_TOP, - loading: isLoading, - items: enhanceDevices(selectOverviewValue.withType(props, domainId, 'devices')), - onMore: () => props.setRoute(route.ROUTE_DEVICES) - }), - - h(CardList, { - headline: 'Browsers', - range: RANGES_LAST_24_HOURS, - sorting: SORTINGS_TOP, - loading: isLoading, - items: enhanceBrowsers(selectOverviewValue.withType(props, domainId, 'browsers')), - onMore: () => props.setRoute(route.ROUTE_BROWSERS) - }), - - h(CardList, { - headline: 'Sizes', - range: RANGES_LAST_24_HOURS, - sorting: SORTINGS_TOP, - loading: isLoading, - items: enhanceSizes(selectOverviewValue.withType(props, domainId, 'sizes')), - onMore: () => props.setRoute(route.ROUTE_SIZES) - }), - - h(CardList, { - headline: 'Languages', - range: RANGES_LAST_24_HOURS, - sorting: SORTINGS_TOP, - loading: isLoading, - items: enhanceLanguages(selectOverviewValue.withType(props, domainId, 'languages')), - onMore: () => props.setRoute(route.ROUTE_LANGUAGES) - }) + h('div', { className: 'content__spacer' }) + + // h(RendererChart, { + // wide: true, + // headline: 'Views', + // interval: INTERVALS_DAILY, + // loading: isLoading, + // items: enhanceViews(selectOverviewValue.withType(props, domainId, 'views'), 14), + // formatter: formatNumber, + // onMore: () => props.setRoute(route.ROUTE_VIEWS) + // }), + + // h(RendererChart, { + // wide: true, + // headline: 'Durations', + // interval: INTERVALS_DAILY, + // loading: isLoading, + // items: enhanceDurations(selectOverviewValue.withType(props, domainId, 'durations'), 14), + // formatter: (ms) => formatDuration(ms).toString(), + // onMore: () => props.setRoute(route.ROUTE_DURATIONS) + // }), + + // h(RendererList, { + // headline: 'Pages', + // range: RANGES_LAST_24_HOURS, + // sorting: SORTINGS_TOP, + // loading: isLoading, + // items: enhancePages(selectOverviewValue.withType(props, domainId, 'pages')), + // onMore: () => props.setRoute(route.ROUTE_PAGES) + // }), + + // h(RendererReferrers, { + // headline: 'Referrers', + // range: RANGES_LAST_24_HOURS, + // sorting: SORTINGS_TOP, + // loading: isLoading, + // items: enhanceReferrers(selectOverviewValue.withType(props, domainId, 'referrers')), + // onMore: () => props.setRoute(route.ROUTE_REFERRERS) + // }), + + // h('div', { className: 'content__spacer' }), + + // h(RendererList, { + // headline: 'Systems', + // range: RANGES_LAST_24_HOURS, + // sorting: SORTINGS_TOP, + // loading: isLoading, + // items: enhanceSystems(selectOverviewValue.withType(props, domainId, 'systems')), + // onMore: () => props.setRoute(route.ROUTE_SYSTEMS) + // }), + + // h(RendererList, { + // headline: 'Devices', + // range: RANGES_LAST_24_HOURS, + // sorting: SORTINGS_TOP, + // loading: isLoading, + // items: enhanceDevices(selectOverviewValue.withType(props, domainId, 'devices')), + // onMore: () => props.setRoute(route.ROUTE_DEVICES) + // }), + + // h(RendererList, { + // headline: 'Browsers', + // range: RANGES_LAST_24_HOURS, + // sorting: SORTINGS_TOP, + // loading: isLoading, + // items: enhanceBrowsers(selectOverviewValue.withType(props, domainId, 'browsers')), + // onMore: () => props.setRoute(route.ROUTE_BROWSERS) + // }), + + // h(RendererList, { + // headline: 'Sizes', + // range: RANGES_LAST_24_HOURS, + // sorting: SORTINGS_TOP, + // loading: isLoading, + // items: enhanceSizes(selectOverviewValue.withType(props, domainId, 'sizes')), + // onMore: () => props.setRoute(route.ROUTE_SIZES) + // }), + + // h(RendererList, { + // headline: 'Languages', + // range: RANGES_LAST_24_HOURS, + // sorting: SORTINGS_TOP, + // loading: isLoading, + // items: enhanceLanguages(selectOverviewValue.withType(props, domainId, 'languages')), + // onMore: () => props.setRoute(route.ROUTE_LANGUAGES) + // }) ) ) diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index bc835633..db9944ec 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -1,32 +1,33 @@ import { createElement as h, Fragment } from 'react' -import CardChart from '../cards/CardChart' +// import RendererChart from '../renderers/RendererChart' -import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' +// import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' import viewsLoader from '../../loaders/viewsLoader' -import mergeViews from '../../utils/mergeViews' +// import mergeViews from '../../utils/mergeViews' import useWidgets from '../../utils/useWidgets' -import formatNumber from '../../utils/formatNumber' +// import formatNumber from '../../utils/formatNumber' const RouteViews = (props) => { - const { rawWidgets, renderedWidgets } = useWidgets(props, viewsLoader, { + // const { rawWidgets, renderedWidgets } = useWidgets(props, viewsLoader, { + const { renderedWidgets } = useWidgets(props, viewsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) return ( h(Fragment, {}, - h(CardChart, { - wide: true, - headline: ({ - [VIEWS_TYPE_UNIQUE]: 'Site Views', - [VIEWS_TYPE_TOTAL]: 'Page Views' - })[props.filter.viewsType], - interval: props.filter.interval, - items: mergeViews(rawWidgets), - formatter: formatNumber - }), + // h(RendererChart, { + // wide: true, + // headline: ({ + // [VIEWS_TYPE_UNIQUE]: 'Site Views', + // [VIEWS_TYPE_TOTAL]: 'Page Views' + // })[props.filter.viewsType], + // interval: props.filter.interval, + // items: mergeViews(rawWidgets), + // formatter: formatNumber + // }), renderedWidgets ) diff --git a/src/ui/scripts/loaders/browsersLoader.js b/src/ui/scripts/loaders/browsersLoader.js index 7238a303..f65e7a8a 100644 --- a/src/ui/scripts/loaders/browsersLoader.js +++ b/src/ui/scripts/loaders/browsersLoader.js @@ -1,27 +1,8 @@ import { createElement as h } from 'react' -import PropTypes from 'prop-types' -import CardList from '../components/cards/CardList' +import RendererList from '../components/renderers/RendererList' import enhanceBrowsers from '../enhancers/enhanceBrowsers' -const Renderer = (props) => { - return h(CardList, { - headline: props.headline, - range: props.widget.variables.range, - sorting: props.widget.variables.sorting, - stale: props.stale, - items: enhanceBrowsers(props.widget.value), - onMore: props.onMore - }) -} - -Renderer.propTypes = { - headline: PropTypes.string.isRequired, - widget: PropTypes.object.isRequired, - stale: PropTypes.bool.isRequired, - onMore: PropTypes.func -} - export default (domainId, opts) => { // TODO: Improve ids @@ -53,10 +34,11 @@ export default (domainId, opts) => { return { id, - Renderer, + Renderer: RendererList, query, variables, - selector + selector, + enhancer: enhanceBrowsers } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/devicesLoader.js b/src/ui/scripts/loaders/devicesLoader.js index c68bc7dc..fc65fa1d 100644 --- a/src/ui/scripts/loaders/devicesLoader.js +++ b/src/ui/scripts/loaders/devicesLoader.js @@ -1,27 +1,8 @@ import { createElement as h } from 'react' -import PropTypes from 'prop-types' -import CardList from '../components/cards/CardList' +import RendererList from '../components/renderers/RendererList' import enhanceDevices from '../enhancers/enhanceDevices' -const Renderer = (props) => { - return h(CardList, { - headline: props.headline, - range: props.widget.variables.range, - sorting: props.widget.variables.sorting, - stale: props.stale, - items: enhanceDevices(props.widget.value), - onMore: props.onMore - }) -} - -Renderer.propTypes = { - headline: PropTypes.string.isRequired, - widget: PropTypes.object.isRequired, - stale: PropTypes.bool.isRequired, - onMore: PropTypes.func -} - export default (domainId, opts) => { // TODO: Improve ids @@ -53,10 +34,11 @@ export default (domainId, opts) => { return { id, - Renderer, + Renderer: RendererList, query, variables, - selector + selector, + enhancer: enhanceDevices } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/durationsLoader.js b/src/ui/scripts/loaders/durationsLoader.js index 1145653b..593107e9 100644 --- a/src/ui/scripts/loaders/durationsLoader.js +++ b/src/ui/scripts/loaders/durationsLoader.js @@ -1,29 +1,9 @@ import { createElement as h } from 'react' -import PropTypes from 'prop-types' -import CardChart from '../components/cards/CardChart' +import RendererChart from '../components/renderers/RendererChart' import enhanceDurations from '../enhancers/enhanceDurations' import formatDuration from '../utils/formatDuration' -const Renderer = (props) => { - return h(CardChart, { - headline: props.headline, - interval: props.widget.variables.interval, - sorting: props.widget.variables.sorting, - stale: props.stale, - items: enhanceDurations(props.widget.value, 7), - formatter: (ms) => formatDuration(ms).toString(), - onMore: props.onMore - }) -} - -Renderer.propTypes = { - headline: PropTypes.string.isRequired, - widget: PropTypes.object.isRequired, - stale: PropTypes.bool.isRequired, - onMore: PropTypes.func -} - export default (domainId, opts) => { // TODO: Improve ids @@ -52,10 +32,14 @@ export default (domainId, opts) => { return { id, - Renderer, + Renderer: (props) => h(RendererChart, { + ...props, + formatter: (ms) => formatDuration(ms).toString() + }), query, variables, - selector + selector, + enhancer: (durations) => enhanceDurations(durations, 7) } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/languagesLoader.js b/src/ui/scripts/loaders/languagesLoader.js index 8b8fbacd..2d127269 100644 --- a/src/ui/scripts/loaders/languagesLoader.js +++ b/src/ui/scripts/loaders/languagesLoader.js @@ -1,27 +1,8 @@ import { createElement as h } from 'react' -import PropTypes from 'prop-types' -import CardList from '../components/cards/CardList' +import RendererList from '../components/renderers/RendererList' import enhanceLanguages from '../enhancers/enhanceLanguages' -const Renderer = (props) => { - return h(CardList, { - headline: props.headline, - range: props.widget.variables.range, - sorting: props.widget.variables.sorting, - stale: props.stale, - items: enhanceLanguages(props.widget.value), - onMore: props.onMore - }) -} - -Renderer.propTypes = { - headline: PropTypes.string.isRequired, - widget: PropTypes.object.isRequired, - stale: PropTypes.bool.isRequired, - onMore: PropTypes.func -} - export default (domainId, opts) => { // TODO: Improve ids @@ -52,10 +33,11 @@ export default (domainId, opts) => { return { id, - Renderer, + Renderer: RendererList, query, variables, - selector + selector, + enhancer: enhanceLanguages } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/pagesLoader.js b/src/ui/scripts/loaders/pagesLoader.js index 3346dec7..88afc0ad 100644 --- a/src/ui/scripts/loaders/pagesLoader.js +++ b/src/ui/scripts/loaders/pagesLoader.js @@ -1,27 +1,8 @@ import { createElement as h } from 'react' -import PropTypes from 'prop-types' -import CardList from '../components/cards/CardList' +import RendererList from '../components/renderers/RendererList' import enhancePages from '../enhancers/enhancePages' -const Renderer = (props) => { - return h(CardList, { - headline: props.headline, - range: props.widget.variables.range, - sorting: props.widget.variables.sorting, - stale: props.stale, - items: enhancePages(props.widget.value), - onMore: props.onMore - }) -} - -Renderer.propTypes = { - headline: PropTypes.string.isRequired, - widget: PropTypes.object.isRequired, - stale: PropTypes.bool.isRequired, - onMore: PropTypes.func -} - export default (domainId, opts) => { // TODO: Improve ids @@ -52,10 +33,11 @@ export default (domainId, opts) => { return { id, - Renderer, + Renderer: RendererList, query, variables, - selector + selector, + enhancer: enhancePages } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/referrersLoader.js b/src/ui/scripts/loaders/referrersLoader.js index a93c42c2..5b898157 100644 --- a/src/ui/scripts/loaders/referrersLoader.js +++ b/src/ui/scripts/loaders/referrersLoader.js @@ -1,27 +1,8 @@ import { createElement as h } from 'react' -import PropTypes from 'prop-types' -import CardReferrers from '../components/cards/CardReferrers' +import RendererReferrers from '../components/renderers/RendererReferrers' import enhanceReferrers from '../enhancers/enhanceReferrers' -const Renderer = (props) => { - return h(CardReferrers, { - headline: props.headline, - range: props.widget.variables.range, - sorting: props.widget.variables.sorting, - stale: props.stale, - items: enhanceReferrers(props.widget.value), - onMore: props.onMore - }) -} - -Renderer.propTypes = { - headline: PropTypes.string.isRequired, - widget: PropTypes.object.isRequired, - stale: PropTypes.bool.isRequired, - onMore: PropTypes.func -} - export default (domainId, opts) => { // TODO: Improve ids @@ -52,10 +33,11 @@ export default (domainId, opts) => { return { id, - Renderer, + Renderer: RendererReferrers, query, variables, - selector + selector, + enhancer: enhanceReferrers } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/sizesLoader.js b/src/ui/scripts/loaders/sizesLoader.js index ca5097c3..9a358f1f 100644 --- a/src/ui/scripts/loaders/sizesLoader.js +++ b/src/ui/scripts/loaders/sizesLoader.js @@ -1,27 +1,8 @@ import { createElement as h } from 'react' -import PropTypes from 'prop-types' -import CardList from '../components/cards/CardList' +import RendererList from '../components/renderers/RendererList' import enhanceSizes from '../enhancers/enhanceSizes' -const Renderer = (props) => { - return h(CardList, { - headline: props.headline, - range: props.widget.variables.range, - sorting: props.widget.variables.sorting, - stale: props.stale, - items: enhanceSizes(props.widget.value), - onMore: props.onMore - }) -} - -Renderer.propTypes = { - headline: PropTypes.string.isRequired, - widget: PropTypes.object.isRequired, - stale: PropTypes.bool.isRequired, - onMore: PropTypes.func -} - export default (domainId, opts) => { // TODO: Improve ids @@ -53,10 +34,11 @@ export default (domainId, opts) => { return { id, - Renderer, + Renderer: RendererList, query, variables, - selector + selector, + enhancer: enhanceSizes } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/systemsLoader.js b/src/ui/scripts/loaders/systemsLoader.js index 769f6104..3f14b805 100644 --- a/src/ui/scripts/loaders/systemsLoader.js +++ b/src/ui/scripts/loaders/systemsLoader.js @@ -1,27 +1,8 @@ import { createElement as h } from 'react' -import PropTypes from 'prop-types' -import CardList from '../components/cards/CardList' +import RendererList from '../components/renderers/RendererList' import enhanceSystems from '../enhancers/enhanceSystems' -const Renderer = (props) => { - return h(CardList, { - headline: props.headline, - range: props.widget.variables.range, - sorting: props.widget.variables.sorting, - stale: props.stale, - items: enhanceSystems(props.widget.value), - onMore: props.onMore - }) -} - -Renderer.propTypes = { - headline: PropTypes.string.isRequired, - widget: PropTypes.object.isRequired, - stale: PropTypes.bool.isRequired, - onMore: PropTypes.func -} - export default (domainId, opts) => { // TODO: Improve ids @@ -53,10 +34,11 @@ export default (domainId, opts) => { return { id, - Renderer, + Renderer: RendererList, query, variables, - selector + selector, + enhancer: enhanceSystems } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js index d264d991..899710e9 100644 --- a/src/ui/scripts/loaders/viewsLoader.js +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -1,29 +1,9 @@ import { createElement as h } from 'react' -import PropTypes from 'prop-types' -import CardChart from '../components/cards/CardChart' +import RendererChart from '../components/renderers/RendererChart' import enhanceViews from '../enhancers/enhanceViews' import formatNumber from '../utils/formatNumber' -const Renderer = (props) => { - return h(CardChart, { - headline: props.headline, - interval: props.widget.variables.interval, - sorting: props.widget.variables.sorting, - stale: props.stale, - items: enhanceViews(props.widget.value, 7), - formatter: formatNumber, - onMore: props.onMore - }) -} - -Renderer.propTypes = { - headline: PropTypes.string.isRequired, - widget: PropTypes.object.isRequired, - stale: PropTypes.bool.isRequired, - onMore: PropTypes.func -} - export default (domainId, opts) => { // TODO: Improve ids @@ -53,10 +33,14 @@ export default (domainId, opts) => { return { id, - Renderer, + Renderer: (props) => h(RendererChart, { + ...props, + formatter: formatNumber + }), query, variables, - selector + selector, + enhancer: (views) => enhanceViews(views, 7) } } \ No newline at end of file From 45197d41fea3693802c6bc47837255fd63592cea Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 5 Dec 2020 18:35:21 +0100 Subject: [PATCH 057/208] Remove unused id from query --- src/ui/scripts/loaders/browsersLoader.js | 1 - src/ui/scripts/loaders/devicesLoader.js | 1 - src/ui/scripts/loaders/durationsLoader.js | 1 - src/ui/scripts/loaders/languagesLoader.js | 1 - src/ui/scripts/loaders/pagesLoader.js | 1 - src/ui/scripts/loaders/referrersLoader.js | 1 - src/ui/scripts/loaders/sizesLoader.js | 1 - src/ui/scripts/loaders/systemsLoader.js | 1 - src/ui/scripts/loaders/viewsLoader.js | 1 - 9 files changed, 9 deletions(-) diff --git a/src/ui/scripts/loaders/browsersLoader.js b/src/ui/scripts/loaders/browsersLoader.js index f65e7a8a..c80a342f 100644 --- a/src/ui/scripts/loaders/browsersLoader.js +++ b/src/ui/scripts/loaders/browsersLoader.js @@ -11,7 +11,6 @@ export default (domainId, opts) => { const query = ` query fetchBrowsers($domainId: ID!, $sorting: Sorting!, $type: BrowserType!, $range: Range) { domain(id: $domainId) { - id statistics { browsers(sorting: $sorting, type: $type, range: $range) { id diff --git a/src/ui/scripts/loaders/devicesLoader.js b/src/ui/scripts/loaders/devicesLoader.js index fc65fa1d..c9caf4b2 100644 --- a/src/ui/scripts/loaders/devicesLoader.js +++ b/src/ui/scripts/loaders/devicesLoader.js @@ -11,7 +11,6 @@ export default (domainId, opts) => { const query = ` query fetchDevices($domainId: ID!, $sorting: Sorting!, $type: DeviceType!, $range: Range) { domain(id: $domainId) { - id statistics { devices(sorting: $sorting, type: $type, range: $range) { id diff --git a/src/ui/scripts/loaders/durationsLoader.js b/src/ui/scripts/loaders/durationsLoader.js index 593107e9..cecc7e69 100644 --- a/src/ui/scripts/loaders/durationsLoader.js +++ b/src/ui/scripts/loaders/durationsLoader.js @@ -12,7 +12,6 @@ export default (domainId, opts) => { const query = ` query fetchDurations($domainId: ID!, $interval: Interval!) { domain(id: $domainId) { - id statistics { durations(interval: $interval) { id diff --git a/src/ui/scripts/loaders/languagesLoader.js b/src/ui/scripts/loaders/languagesLoader.js index 2d127269..bb3df7a4 100644 --- a/src/ui/scripts/loaders/languagesLoader.js +++ b/src/ui/scripts/loaders/languagesLoader.js @@ -11,7 +11,6 @@ export default (domainId, opts) => { const query = ` query fetchLanguages($domainId: ID!, $sorting: Sorting!, $range: Range) { domain(id: $domainId) { - id statistics { languages(sorting: $sorting, range: $range) { id diff --git a/src/ui/scripts/loaders/pagesLoader.js b/src/ui/scripts/loaders/pagesLoader.js index 88afc0ad..6267deb3 100644 --- a/src/ui/scripts/loaders/pagesLoader.js +++ b/src/ui/scripts/loaders/pagesLoader.js @@ -11,7 +11,6 @@ export default (domainId, opts) => { const query = ` query fetchPages($domainId: ID!, $sorting: Sorting!, $range: Range) { domain(id: $domainId) { - id statistics { pages(sorting: $sorting, range: $range) { id diff --git a/src/ui/scripts/loaders/referrersLoader.js b/src/ui/scripts/loaders/referrersLoader.js index 5b898157..44588c3c 100644 --- a/src/ui/scripts/loaders/referrersLoader.js +++ b/src/ui/scripts/loaders/referrersLoader.js @@ -11,7 +11,6 @@ export default (domainId, opts) => { const query = ` query fetchReferrers($domainId: ID!, $sorting: Sorting!, $range: Range) { domain(id: $domainId) { - id statistics { referrers(sorting: $sorting, range: $range) { id diff --git a/src/ui/scripts/loaders/sizesLoader.js b/src/ui/scripts/loaders/sizesLoader.js index 9a358f1f..9d79a44c 100644 --- a/src/ui/scripts/loaders/sizesLoader.js +++ b/src/ui/scripts/loaders/sizesLoader.js @@ -11,7 +11,6 @@ export default (domainId, opts) => { const query = ` query fetchSizes($domainId: ID!, $sorting: Sorting!, $type: SizeType!, $range: Range) { domain(id: $domainId) { - id statistics { sizes(sorting: $sorting, type: $type, range: $range) { id diff --git a/src/ui/scripts/loaders/systemsLoader.js b/src/ui/scripts/loaders/systemsLoader.js index 3f14b805..93ff17ab 100644 --- a/src/ui/scripts/loaders/systemsLoader.js +++ b/src/ui/scripts/loaders/systemsLoader.js @@ -11,7 +11,6 @@ export default (domainId, opts) => { const query = ` query fetchSystems($domainId: ID!, $sorting: Sorting!, $type: SystemType!, $range: Range) { domain(id: $domainId) { - id statistics { systems(sorting: $sorting, type: $type, range: $range) { id diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js index 899710e9..ae9cb787 100644 --- a/src/ui/scripts/loaders/viewsLoader.js +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -12,7 +12,6 @@ export default (domainId, opts) => { const query = ` query fetchViews($domainId: ID!, $interval: Interval!, $type: ViewType!) { domain(id: $domainId) { - id statistics { views(interval: $interval, type: $type) { id From 16b474964905993ca89dbd97074d519a535f3085 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 5 Dec 2020 18:37:03 +0100 Subject: [PATCH 058/208] Return rendered widgets directly in useWidgets --- src/ui/scripts/components/routes/RouteBrowsers.js | 4 +--- src/ui/scripts/components/routes/RouteDevices.js | 4 +--- .../scripts/components/routes/RouteLanguages.js | 4 +--- src/ui/scripts/components/routes/RoutePages.js | 4 +--- .../scripts/components/routes/RouteReferrers.js | 4 +--- src/ui/scripts/components/routes/RouteSizes.js | 4 +--- src/ui/scripts/components/routes/RouteSystems.js | 4 +--- src/ui/scripts/utils/useWidgets.js | 15 +++------------ 8 files changed, 10 insertions(+), 33 deletions(-) diff --git a/src/ui/scripts/components/routes/RouteBrowsers.js b/src/ui/scripts/components/routes/RouteBrowsers.js index a767c7ca..cc720319 100644 --- a/src/ui/scripts/components/routes/RouteBrowsers.js +++ b/src/ui/scripts/components/routes/RouteBrowsers.js @@ -5,14 +5,12 @@ import useWidgets from '../../utils/useWidgets' const RouteBrowsers = (props) => { - const { renderedWidgets } = useWidgets(props, browsersLoader, { + return useWidgets(props, browsersLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.browsersType }) - return renderedWidgets - } export default RouteBrowsers \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteDevices.js b/src/ui/scripts/components/routes/RouteDevices.js index a611a801..76a33d1d 100644 --- a/src/ui/scripts/components/routes/RouteDevices.js +++ b/src/ui/scripts/components/routes/RouteDevices.js @@ -5,14 +5,12 @@ import useWidgets from '../../utils/useWidgets' const RouteDevices = (props) => { - const { renderedWidgets } = useWidgets(props, devicesLoader, { + return useWidgets(props, devicesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.devicesType }) - return renderedWidgets - } export default RouteDevices \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index 8ec36ea9..e8deaf79 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -5,13 +5,11 @@ import useWidgets from '../../utils/useWidgets' const RouteLanguages = (props) => { - const { renderedWidgets } = useWidgets(props, languagesLoader, { + return useWidgets(props, languagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) - return renderedWidgets - } export default RouteLanguages \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index 0339449e..8d63a428 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -5,13 +5,11 @@ import useWidgets from '../../utils/useWidgets' const RoutePages = (props) => { - const { renderedWidgets } = useWidgets(props, pagesLoader, { + return useWidgets(props, pagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) - return renderedWidgets - } export default RoutePages \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteReferrers.js b/src/ui/scripts/components/routes/RouteReferrers.js index 0dd8b575..33e4ef4f 100644 --- a/src/ui/scripts/components/routes/RouteReferrers.js +++ b/src/ui/scripts/components/routes/RouteReferrers.js @@ -5,13 +5,11 @@ import useWidgets from '../../utils/useWidgets' const RouteReferrers = (props) => { - const { renderedWidgets } = useWidgets(props, referrersLoader, { + return useWidgets(props, referrersLoader, { range: props.filter.range, sorting: props.filter.sorting }) - return renderedWidgets - } export default RouteReferrers \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteSizes.js b/src/ui/scripts/components/routes/RouteSizes.js index da12857f..4529983d 100644 --- a/src/ui/scripts/components/routes/RouteSizes.js +++ b/src/ui/scripts/components/routes/RouteSizes.js @@ -5,14 +5,12 @@ import useWidgets from '../../utils/useWidgets' const RouteSizes = (props) => { - const { renderedWidgets } = useWidgets(props, sizesLoader, { + return useWidgets(props, sizesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.sizesType }) - return renderedWidgets - } export default RouteSizes \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteSystems.js b/src/ui/scripts/components/routes/RouteSystems.js index e6819f12..382da2b3 100644 --- a/src/ui/scripts/components/routes/RouteSystems.js +++ b/src/ui/scripts/components/routes/RouteSystems.js @@ -5,14 +5,12 @@ import useWidgets from '../../utils/useWidgets' const RouteSystems = (props) => { - const { renderedWidgets } = useWidgets(props, systemsLoader, { + return useWidgets(props, systemsLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.systemsType }) - return renderedWidgets - } export default RouteSystems \ No newline at end of file diff --git a/src/ui/scripts/utils/useWidgets.js b/src/ui/scripts/utils/useWidgets.js index 63933937..c56cb6a8 100644 --- a/src/ui/scripts/utils/useWidgets.js +++ b/src/ui/scripts/utils/useWidgets.js @@ -25,13 +25,9 @@ export default (props, createLoader, opts) => { }, [ props.domains.value, ...Object.values(opts) ]) - const rawWidgets = widgetIds.map((widgetId) => { - const widget = props.widgets.value[widgetId] - return widget == null ? initialSubState() : widget - }) - - const renderedWidgets = rawWidgets.map( - (widgetData) => { + return widgetIds.map( + (widgetId) => { + const widgetData = props.widgets.value[widgetId] || initialSubState() const domain = selectDomainsValue.byId(props, widgetData.variables.domainId) return h(CardWidget, { @@ -43,9 +39,4 @@ export default (props, createLoader, opts) => { } ) - return { - rawWidgets, - renderedWidgets - } - } \ No newline at end of file From 1933adb60974839bc2919a18688db2354bfd498e Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 5 Dec 2020 19:03:58 +0100 Subject: [PATCH 059/208] Merged widgets --- src/ui/scripts/components/cards/CardWidget.js | 14 +++++-- .../components/routes/RouteDurations.js | 26 ++++++------ .../scripts/components/routes/RouteViews.js | 36 ++++++++-------- src/ui/scripts/loaders/durationsLoader.js | 2 +- .../scripts/loaders/mergedDurationsLoader.js | 41 ++++++++++++++++++ src/ui/scripts/loaders/mergedViewsLoader.js | 42 +++++++++++++++++++ src/ui/scripts/loaders/viewsLoader.js | 2 +- src/ui/scripts/utils/useMergedWidget.js | 28 +++++++++++++ 8 files changed, 154 insertions(+), 37 deletions(-) create mode 100644 src/ui/scripts/loaders/mergedDurationsLoader.js create mode 100644 src/ui/scripts/loaders/mergedViewsLoader.js create mode 100644 src/ui/scripts/utils/useMergedWidget.js diff --git a/src/ui/scripts/components/cards/CardWidget.js b/src/ui/scripts/components/cards/CardWidget.js index 13e60213..d12bf726 100644 --- a/src/ui/scripts/components/cards/CardWidget.js +++ b/src/ui/scripts/components/cards/CardWidget.js @@ -16,7 +16,12 @@ const CardWidget = (props) => { isLoading } = status(props.widget.value, props.widget.fetching) - const [ textLabel, setTextLabel ] = useState('') + const headline = typeof props.headline === 'function' ? + props.headline(props.widget) : + props.headline + + // Use thin space as initial value to avoid that the label changes the height once rendered + const [ textLabel, setTextLabel ] = useState(' ') const presentation = (() => { if (isLoading === true) { @@ -49,7 +54,7 @@ const CardWidget = (props) => { type: 'h2', size: 'medium', onClick: props.onMore - }, props.headline), + }, headline), h(Text, { type: 'div', spacing: false @@ -63,7 +68,10 @@ const CardWidget = (props) => { CardWidget.propTypes = { wide: PropTypes.bool, - headline: PropTypes.string.isRequired, + headline: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.func + ]).isRequired, widget: PropTypes.object.isRequired } diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index b10fc720..b7410b07 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -1,30 +1,28 @@ import { createElement as h, Fragment } from 'react' -// import CardChart from '../renderers/RendererChart' - +import mergedDurationsLoader from '../../loaders/mergedDurationsLoader' import durationsLoader from '../../loaders/durationsLoader' -// import mergeDurations from '../../utils/mergeDurations' +import useMergedWidget from '../../utils/useMergedWidget' import useWidgets from '../../utils/useWidgets' -// import formatDuration from '../../utils/formatDuration' const RouteDurations = (props) => { - // const { rawWidgets, renderedWidgets } = useWidgets(props, durationsLoader, { - const { renderedWidgets } = useWidgets(props, durationsLoader, { + const renderedMergedWidget = useMergedWidget(props, mergedDurationsLoader, { + interval: props.filter.interval, + type: props.filter.viewsType + }, { + wide: true, + headline: () => 'Durations' + }) + + const renderedWidgets = useWidgets(props, durationsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) return ( h(Fragment, {}, - // h(CardChart, { - // wide: true, - // headline: 'Durations', - // interval: props.filter.interval, - // items: mergeDurations(rawWidgets), - // formatter: (ms) => formatDuration(ms).toString() - // }), - + renderedMergedWidget, renderedWidgets ) ) diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index db9944ec..e6751fed 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -1,34 +1,34 @@ import { createElement as h, Fragment } from 'react' -// import RendererChart from '../renderers/RendererChart' - -// import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' +import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' +import mergedViewsLoader from '../../loaders/mergedViewsLoader' import viewsLoader from '../../loaders/viewsLoader' -// import mergeViews from '../../utils/mergeViews' +import useMergedWidget from '../../utils/useMergedWidget' import useWidgets from '../../utils/useWidgets' -// import formatNumber from '../../utils/formatNumber' const RouteViews = (props) => { - // const { rawWidgets, renderedWidgets } = useWidgets(props, viewsLoader, { - const { renderedWidgets } = useWidgets(props, viewsLoader, { + const renderedMergedWidgets = useMergedWidget(props, mergedViewsLoader, { + interval: props.filter.interval, + type: props.filter.viewsType + }, { + wide: true, + headline: (widget) => { + return ({ + [VIEWS_TYPE_UNIQUE]: 'Site Views', + [VIEWS_TYPE_TOTAL]: 'Page Views' + })[widget.variables.type] + } + }) + + const renderedWidgets = useWidgets(props, viewsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) return ( h(Fragment, {}, - // h(RendererChart, { - // wide: true, - // headline: ({ - // [VIEWS_TYPE_UNIQUE]: 'Site Views', - // [VIEWS_TYPE_TOTAL]: 'Page Views' - // })[props.filter.viewsType], - // interval: props.filter.interval, - // items: mergeViews(rawWidgets), - // formatter: formatNumber - // }), - + renderedMergedWidgets, renderedWidgets ) ) diff --git a/src/ui/scripts/loaders/durationsLoader.js b/src/ui/scripts/loaders/durationsLoader.js index cecc7e69..b8627eb5 100644 --- a/src/ui/scripts/loaders/durationsLoader.js +++ b/src/ui/scripts/loaders/durationsLoader.js @@ -13,7 +13,7 @@ export default (domainId, opts) => { query fetchDurations($domainId: ID!, $interval: Interval!) { domain(id: $domainId) { statistics { - durations(interval: $interval) { + durations(interval: $interval, limit: 7) { id count } diff --git a/src/ui/scripts/loaders/mergedDurationsLoader.js b/src/ui/scripts/loaders/mergedDurationsLoader.js new file mode 100644 index 00000000..3588f06e --- /dev/null +++ b/src/ui/scripts/loaders/mergedDurationsLoader.js @@ -0,0 +1,41 @@ +import { createElement as h } from 'react' + +import RendererChart from '../components/renderers/RendererChart' +import enhanceDurations from '../enhancers/enhanceDurations' +import formatDuration from '../utils/formatDuration' + +export default (opts) => { + + // TODO: Improve ids + const id = `fetchMergedDurations${ JSON.stringify(opts) }` + + const query = ` + query fetchDurations($interval: Interval!) { + statistics { + durations(interval: $interval) { + id + count + } + } + } + ` + + const variables = { + interval: opts.interval + } + + const selector = (data) => data.statistics.durations + + return { + id, + Renderer: (props) => h(RendererChart, { + ...props, + formatter: (ms) => formatDuration(ms).toString() + }), + query, + variables, + selector, + enhancer: (durations) => enhanceDurations(durations, 14) + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/mergedViewsLoader.js b/src/ui/scripts/loaders/mergedViewsLoader.js new file mode 100644 index 00000000..1f1eb24a --- /dev/null +++ b/src/ui/scripts/loaders/mergedViewsLoader.js @@ -0,0 +1,42 @@ +import { createElement as h } from 'react' + +import RendererChart from '../components/renderers/RendererChart' +import enhanceViews from '../enhancers/enhanceViews' +import formatNumber from '../utils/formatNumber' + +export default (opts) => { + + // TODO: Improve ids + const id = `fetchMergedViews${ JSON.stringify(opts) }` + + const query = ` + query fetchViews($interval: Interval!, $type: ViewType!) { + statistics { + views(interval: $interval, type: $type) { + id + count + } + } + } + ` + + const variables = { + interval: opts.interval, + type: opts.type + } + + const selector = (data) => data.statistics.views + + return { + id, + Renderer: (props) => h(RendererChart, { + ...props, + formatter: formatNumber + }), + query, + variables, + selector, + enhancer: (views) => enhanceViews(views, 14) + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js index ae9cb787..6605eb55 100644 --- a/src/ui/scripts/loaders/viewsLoader.js +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -13,7 +13,7 @@ export default (domainId, opts) => { query fetchViews($domainId: ID!, $interval: Interval!, $type: ViewType!) { domain(id: $domainId) { statistics { - views(interval: $interval, type: $type) { + views(interval: $interval, type: $type, limit: 7) { id count } diff --git a/src/ui/scripts/utils/useMergedWidget.js b/src/ui/scripts/utils/useMergedWidget.js new file mode 100644 index 00000000..d9e8515e --- /dev/null +++ b/src/ui/scripts/utils/useMergedWidget.js @@ -0,0 +1,28 @@ +import { createElement as h, useEffect, useState } from 'react' + +import CardWidget from '../components/cards/CardWidget' + +import { initialSubState } from '../reducers/widgets' + +export default (props, createLoader, opts, additionalProps = {}) => { + + const [ widgetId, setWidgetId ] = useState() + + useEffect(() => { + + const loader = createLoader(opts) + props.fetchWidget(props, loader) + + setWidgetId(loader.id) + + }, [ ...Object.values(opts) ]) + + const widgetData = props.widgets.value[widgetId] || initialSubState() + + return h(CardWidget, { + key: widgetId, + widget: widgetData, + ...additionalProps + }) + +} \ No newline at end of file From 804fe87c3fc7fd442e451d618317a47bb572b183 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 13:42:35 +0100 Subject: [PATCH 060/208] Improve ids --- src/ui/scripts/loaders/browsersLoader.js | 4 ++-- src/ui/scripts/loaders/devicesLoader.js | 4 ++-- src/ui/scripts/loaders/durationsLoader.js | 4 ++-- src/ui/scripts/loaders/languagesLoader.js | 4 ++-- .../scripts/loaders/mergedDurationsLoader.js | 6 ++--- src/ui/scripts/loaders/mergedViewsLoader.js | 6 ++--- src/ui/scripts/loaders/pagesLoader.js | 4 ++-- src/ui/scripts/loaders/referrersLoader.js | 4 ++-- src/ui/scripts/loaders/sizesLoader.js | 4 ++-- src/ui/scripts/loaders/systemsLoader.js | 4 ++-- src/ui/scripts/loaders/viewsLoader.js | 4 ++-- src/ui/scripts/utils/createWidgetId.js | 23 +++++++++++++++++++ 12 files changed, 47 insertions(+), 24 deletions(-) create mode 100644 src/ui/scripts/utils/createWidgetId.js diff --git a/src/ui/scripts/loaders/browsersLoader.js b/src/ui/scripts/loaders/browsersLoader.js index c80a342f..75663368 100644 --- a/src/ui/scripts/loaders/browsersLoader.js +++ b/src/ui/scripts/loaders/browsersLoader.js @@ -2,11 +2,11 @@ import { createElement as h } from 'react' import RendererList from '../components/renderers/RendererList' import enhanceBrowsers from '../enhancers/enhanceBrowsers' +import createWidgetId from '../utils/createWidgetId' export default (domainId, opts) => { - // TODO: Improve ids - const id = `fetchBrowsers${ domainId }${ JSON.stringify(opts) }` + const id = createWidgetId('fetchBrowsers', domainId, opts) const query = ` query fetchBrowsers($domainId: ID!, $sorting: Sorting!, $type: BrowserType!, $range: Range) { diff --git a/src/ui/scripts/loaders/devicesLoader.js b/src/ui/scripts/loaders/devicesLoader.js index c9caf4b2..2584d971 100644 --- a/src/ui/scripts/loaders/devicesLoader.js +++ b/src/ui/scripts/loaders/devicesLoader.js @@ -2,11 +2,11 @@ import { createElement as h } from 'react' import RendererList from '../components/renderers/RendererList' import enhanceDevices from '../enhancers/enhanceDevices' +import createWidgetId from '../utils/createWidgetId' export default (domainId, opts) => { - // TODO: Improve ids - const id = `fetchDevices${ domainId }${ JSON.stringify(opts) }` + const id = createWidgetId('fetchDevices', domainId, opts) const query = ` query fetchDevices($domainId: ID!, $sorting: Sorting!, $type: DeviceType!, $range: Range) { diff --git a/src/ui/scripts/loaders/durationsLoader.js b/src/ui/scripts/loaders/durationsLoader.js index b8627eb5..8fe926a4 100644 --- a/src/ui/scripts/loaders/durationsLoader.js +++ b/src/ui/scripts/loaders/durationsLoader.js @@ -3,11 +3,11 @@ import { createElement as h } from 'react' import RendererChart from '../components/renderers/RendererChart' import enhanceDurations from '../enhancers/enhanceDurations' import formatDuration from '../utils/formatDuration' +import createWidgetId from '../utils/createWidgetId' export default (domainId, opts) => { - // TODO: Improve ids - const id = `fetchDurations${ domainId }${ JSON.stringify(opts) }` + const id = createWidgetId('fetchDurations', domainId, opts) const query = ` query fetchDurations($domainId: ID!, $interval: Interval!) { diff --git a/src/ui/scripts/loaders/languagesLoader.js b/src/ui/scripts/loaders/languagesLoader.js index bb3df7a4..7715e6c3 100644 --- a/src/ui/scripts/loaders/languagesLoader.js +++ b/src/ui/scripts/loaders/languagesLoader.js @@ -2,11 +2,11 @@ import { createElement as h } from 'react' import RendererList from '../components/renderers/RendererList' import enhanceLanguages from '../enhancers/enhanceLanguages' +import createWidgetId from '../utils/createWidgetId' export default (domainId, opts) => { - // TODO: Improve ids - const id = `fetchLanguages${ domainId }${ JSON.stringify(opts) }` + const id = createWidgetId('fetchLanguages', domainId, opts) const query = ` query fetchLanguages($domainId: ID!, $sorting: Sorting!, $range: Range) { diff --git a/src/ui/scripts/loaders/mergedDurationsLoader.js b/src/ui/scripts/loaders/mergedDurationsLoader.js index 3588f06e..b993c6f2 100644 --- a/src/ui/scripts/loaders/mergedDurationsLoader.js +++ b/src/ui/scripts/loaders/mergedDurationsLoader.js @@ -3,14 +3,14 @@ import { createElement as h } from 'react' import RendererChart from '../components/renderers/RendererChart' import enhanceDurations from '../enhancers/enhanceDurations' import formatDuration from '../utils/formatDuration' +import createWidgetId from '../utils/createWidgetId' export default (opts) => { - // TODO: Improve ids - const id = `fetchMergedDurations${ JSON.stringify(opts) }` + const id = createWidgetId('fetchMergedDurations', undefined, opts) const query = ` - query fetchDurations($interval: Interval!) { + query fetchMergedDurations($interval: Interval!) { statistics { durations(interval: $interval) { id diff --git a/src/ui/scripts/loaders/mergedViewsLoader.js b/src/ui/scripts/loaders/mergedViewsLoader.js index 1f1eb24a..9a538c95 100644 --- a/src/ui/scripts/loaders/mergedViewsLoader.js +++ b/src/ui/scripts/loaders/mergedViewsLoader.js @@ -3,14 +3,14 @@ import { createElement as h } from 'react' import RendererChart from '../components/renderers/RendererChart' import enhanceViews from '../enhancers/enhanceViews' import formatNumber from '../utils/formatNumber' +import createWidgetId from '../utils/createWidgetId' export default (opts) => { - // TODO: Improve ids - const id = `fetchMergedViews${ JSON.stringify(opts) }` + const id = createWidgetId('fetchMergedViews', undefined, opts) const query = ` - query fetchViews($interval: Interval!, $type: ViewType!) { + query fetchMergedViews($interval: Interval!, $type: ViewType!) { statistics { views(interval: $interval, type: $type) { id diff --git a/src/ui/scripts/loaders/pagesLoader.js b/src/ui/scripts/loaders/pagesLoader.js index 6267deb3..95962887 100644 --- a/src/ui/scripts/loaders/pagesLoader.js +++ b/src/ui/scripts/loaders/pagesLoader.js @@ -2,11 +2,11 @@ import { createElement as h } from 'react' import RendererList from '../components/renderers/RendererList' import enhancePages from '../enhancers/enhancePages' +import createWidgetId from '../utils/createWidgetId' export default (domainId, opts) => { - // TODO: Improve ids - const id = `fetchPages${ domainId }${ JSON.stringify(opts) }` + const id = createWidgetId('fetchPages', domainId, opts) const query = ` query fetchPages($domainId: ID!, $sorting: Sorting!, $range: Range) { diff --git a/src/ui/scripts/loaders/referrersLoader.js b/src/ui/scripts/loaders/referrersLoader.js index 44588c3c..f0ad79d9 100644 --- a/src/ui/scripts/loaders/referrersLoader.js +++ b/src/ui/scripts/loaders/referrersLoader.js @@ -2,11 +2,11 @@ import { createElement as h } from 'react' import RendererReferrers from '../components/renderers/RendererReferrers' import enhanceReferrers from '../enhancers/enhanceReferrers' +import createWidgetId from '../utils/createWidgetId' export default (domainId, opts) => { - // TODO: Improve ids - const id = `fetchReferrers${ domainId }${ JSON.stringify(opts) }` + const id = createWidgetId('fetchReferrers', domainId, opts) const query = ` query fetchReferrers($domainId: ID!, $sorting: Sorting!, $range: Range) { diff --git a/src/ui/scripts/loaders/sizesLoader.js b/src/ui/scripts/loaders/sizesLoader.js index 9d79a44c..6250daff 100644 --- a/src/ui/scripts/loaders/sizesLoader.js +++ b/src/ui/scripts/loaders/sizesLoader.js @@ -2,11 +2,11 @@ import { createElement as h } from 'react' import RendererList from '../components/renderers/RendererList' import enhanceSizes from '../enhancers/enhanceSizes' +import createWidgetId from '../utils/createWidgetId' export default (domainId, opts) => { - // TODO: Improve ids - const id = `fetchSizes${ domainId }${ JSON.stringify(opts) }` + const id = createWidgetId('fetchSizes', domainId, opts) const query = ` query fetchSizes($domainId: ID!, $sorting: Sorting!, $type: SizeType!, $range: Range) { diff --git a/src/ui/scripts/loaders/systemsLoader.js b/src/ui/scripts/loaders/systemsLoader.js index 93ff17ab..47c32224 100644 --- a/src/ui/scripts/loaders/systemsLoader.js +++ b/src/ui/scripts/loaders/systemsLoader.js @@ -2,11 +2,11 @@ import { createElement as h } from 'react' import RendererList from '../components/renderers/RendererList' import enhanceSystems from '../enhancers/enhanceSystems' +import createWidgetId from '../utils/createWidgetId' export default (domainId, opts) => { - // TODO: Improve ids - const id = `fetchSystems${ domainId }${ JSON.stringify(opts) }` + const id = createWidgetId('fetchSystems', domainId, opts) const query = ` query fetchSystems($domainId: ID!, $sorting: Sorting!, $type: SystemType!, $range: Range) { diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js index 6605eb55..e3a34d30 100644 --- a/src/ui/scripts/loaders/viewsLoader.js +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -3,11 +3,11 @@ import { createElement as h } from 'react' import RendererChart from '../components/renderers/RendererChart' import enhanceViews from '../enhancers/enhanceViews' import formatNumber from '../utils/formatNumber' +import createWidgetId from '../utils/createWidgetId' export default (domainId, opts) => { - // TODO: Improve ids - const id = `fetchViews${ domainId }${ JSON.stringify(opts) }` + const id = createWidgetId('fetchViews', domainId, opts) const query = ` query fetchViews($domainId: ID!, $interval: Interval!, $type: ViewType!) { diff --git a/src/ui/scripts/utils/createWidgetId.js b/src/ui/scripts/utils/createWidgetId.js new file mode 100644 index 00000000..e3be7de3 --- /dev/null +++ b/src/ui/scripts/utils/createWidgetId.js @@ -0,0 +1,23 @@ +import { v4 as uuid } from 'uuid' + +const ids = new Map() + +const existingId = (key) => { + return ids.get(key) +} + +const newId = (key) => { + const id = uuid() + ids.set(key, id) + return id +} + +export default (type, domainId, opts) => { + + const key = `${ type }${ domainId || '' }${ JSON.stringify(opts) }` + const id = existingId(key) + + if (id == null) return newId(key) + return id + +} \ No newline at end of file From e2da84721e6ffc3d0aae13d420da722f36fdb66c Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 13:58:38 +0100 Subject: [PATCH 061/208] Fix name --- src/ui/scripts/components/routes/RouteViews.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index e6751fed..727fc716 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -8,7 +8,7 @@ import useWidgets from '../../utils/useWidgets' const RouteViews = (props) => { - const renderedMergedWidgets = useMergedWidget(props, mergedViewsLoader, { + const renderedMergedWidget = useMergedWidget(props, mergedViewsLoader, { interval: props.filter.interval, type: props.filter.viewsType }, { @@ -28,7 +28,7 @@ const RouteViews = (props) => { return ( h(Fragment, {}, - renderedMergedWidgets, + renderedMergedWidget, renderedWidgets ) ) From 8a4602c7b54603840ba2ce140d03b59f51aa0ae2 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 14:00:05 +0100 Subject: [PATCH 062/208] Remove unused durations type --- src/ui/scripts/components/routes/RouteDurations.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index b7410b07..a1fc9d44 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -8,8 +8,7 @@ import useWidgets from '../../utils/useWidgets' const RouteDurations = (props) => { const renderedMergedWidget = useMergedWidget(props, mergedDurationsLoader, { - interval: props.filter.interval, - type: props.filter.viewsType + interval: props.filter.interval }, { wide: true, headline: () => 'Durations' From 896d81d72c8afde1d3099178dcc829690127ce94 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 14:03:56 +0100 Subject: [PATCH 063/208] Add missing prop types --- src/ui/scripts/components/cards/CardWidget.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/scripts/components/cards/CardWidget.js b/src/ui/scripts/components/cards/CardWidget.js index d12bf726..70d42454 100644 --- a/src/ui/scripts/components/cards/CardWidget.js +++ b/src/ui/scripts/components/cards/CardWidget.js @@ -72,7 +72,8 @@ CardWidget.propTypes = { PropTypes.string, PropTypes.func ]).isRequired, - widget: PropTypes.object.isRequired + widget: PropTypes.object.isRequired, + onMore: PropTypes.func } export default CardWidget \ No newline at end of file From 154bf3d62db7ee1eaf676dc3765410200aa19f95 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 14:09:25 +0100 Subject: [PATCH 064/208] Rename use functions --- src/ui/scripts/components/routes/RouteBrowsers.js | 4 ++-- src/ui/scripts/components/routes/RouteDevices.js | 4 ++-- src/ui/scripts/components/routes/RouteDurations.js | 6 +++--- src/ui/scripts/components/routes/RouteLanguages.js | 4 ++-- src/ui/scripts/components/routes/RoutePages.js | 4 ++-- src/ui/scripts/components/routes/RouteReferrers.js | 4 ++-- src/ui/scripts/components/routes/RouteSizes.js | 4 ++-- src/ui/scripts/components/routes/RouteSystems.js | 4 ++-- src/ui/scripts/components/routes/RouteViews.js | 6 +++--- src/ui/scripts/utils/{useWidgets.js => useDomainWidgets.js} | 5 +++-- 10 files changed, 23 insertions(+), 22 deletions(-) rename src/ui/scripts/utils/{useWidgets.js => useDomainWidgets.js} (86%) diff --git a/src/ui/scripts/components/routes/RouteBrowsers.js b/src/ui/scripts/components/routes/RouteBrowsers.js index cc720319..5616246d 100644 --- a/src/ui/scripts/components/routes/RouteBrowsers.js +++ b/src/ui/scripts/components/routes/RouteBrowsers.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import browsersLoader from '../../loaders/browsersLoader' -import useWidgets from '../../utils/useWidgets' +import useDomainWidgets from '../../utils/useDomainWidgets' const RouteBrowsers = (props) => { - return useWidgets(props, browsersLoader, { + return useDomainWidgets(props, browsersLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.browsersType diff --git a/src/ui/scripts/components/routes/RouteDevices.js b/src/ui/scripts/components/routes/RouteDevices.js index 76a33d1d..48a94c08 100644 --- a/src/ui/scripts/components/routes/RouteDevices.js +++ b/src/ui/scripts/components/routes/RouteDevices.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import devicesLoader from '../../loaders/devicesLoader' -import useWidgets from '../../utils/useWidgets' +import useDomainWidgets from '../../utils/useDomainWidgets' const RouteDevices = (props) => { - return useWidgets(props, devicesLoader, { + return useDomainWidgets(props, devicesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.devicesType diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index a1fc9d44..e0c13322 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -3,7 +3,7 @@ import { createElement as h, Fragment } from 'react' import mergedDurationsLoader from '../../loaders/mergedDurationsLoader' import durationsLoader from '../../loaders/durationsLoader' import useMergedWidget from '../../utils/useMergedWidget' -import useWidgets from '../../utils/useWidgets' +import useDomainWidgets from '../../utils/useDomainWidgets' const RouteDurations = (props) => { @@ -14,7 +14,7 @@ const RouteDurations = (props) => { headline: () => 'Durations' }) - const renderedWidgets = useWidgets(props, durationsLoader, { + const renderedDomainWidgets = useDomainWidgets(props, durationsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) @@ -22,7 +22,7 @@ const RouteDurations = (props) => { return ( h(Fragment, {}, renderedMergedWidget, - renderedWidgets + renderedDomainWidgets ) ) diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index e8deaf79..30f930ba 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import languagesLoader from '../../loaders/languagesLoader' -import useWidgets from '../../utils/useWidgets' +import useDomainWidgets from '../../utils/useDomainWidgets' const RouteLanguages = (props) => { - return useWidgets(props, languagesLoader, { + return useDomainWidgets(props, languagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index 8d63a428..66893b50 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import pagesLoader from '../../loaders/pagesLoader' -import useWidgets from '../../utils/useWidgets' +import useDomainWidgets from '../../utils/useDomainWidgets' const RoutePages = (props) => { - return useWidgets(props, pagesLoader, { + return useDomainWidgets(props, pagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) diff --git a/src/ui/scripts/components/routes/RouteReferrers.js b/src/ui/scripts/components/routes/RouteReferrers.js index 33e4ef4f..a59cb332 100644 --- a/src/ui/scripts/components/routes/RouteReferrers.js +++ b/src/ui/scripts/components/routes/RouteReferrers.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import referrersLoader from '../../loaders/referrersLoader' -import useWidgets from '../../utils/useWidgets' +import useDomainWidgets from '../../utils/useDomainWidgets' const RouteReferrers = (props) => { - return useWidgets(props, referrersLoader, { + return useDomainWidgets(props, referrersLoader, { range: props.filter.range, sorting: props.filter.sorting }) diff --git a/src/ui/scripts/components/routes/RouteSizes.js b/src/ui/scripts/components/routes/RouteSizes.js index 4529983d..9adad409 100644 --- a/src/ui/scripts/components/routes/RouteSizes.js +++ b/src/ui/scripts/components/routes/RouteSizes.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import sizesLoader from '../../loaders/sizesLoader' -import useWidgets from '../../utils/useWidgets' +import useDomainWidgets from '../../utils/useDomainWidgets' const RouteSizes = (props) => { - return useWidgets(props, sizesLoader, { + return useDomainWidgets(props, sizesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.sizesType diff --git a/src/ui/scripts/components/routes/RouteSystems.js b/src/ui/scripts/components/routes/RouteSystems.js index 382da2b3..d5a947ca 100644 --- a/src/ui/scripts/components/routes/RouteSystems.js +++ b/src/ui/scripts/components/routes/RouteSystems.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import systemsLoader from '../../loaders/systemsLoader' -import useWidgets from '../../utils/useWidgets' +import useDomainWidgets from '../../utils/useDomainWidgets' const RouteSystems = (props) => { - return useWidgets(props, systemsLoader, { + return useDomainWidgets(props, systemsLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.systemsType diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 727fc716..08efe59b 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -4,7 +4,7 @@ import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views import mergedViewsLoader from '../../loaders/mergedViewsLoader' import viewsLoader from '../../loaders/viewsLoader' import useMergedWidget from '../../utils/useMergedWidget' -import useWidgets from '../../utils/useWidgets' +import useDomainWidgets from '../../utils/useDomainWidgets' const RouteViews = (props) => { @@ -21,7 +21,7 @@ const RouteViews = (props) => { } }) - const renderedWidgets = useWidgets(props, viewsLoader, { + const renderedDomainWidgets = useDomainWidgets(props, viewsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) @@ -29,7 +29,7 @@ const RouteViews = (props) => { return ( h(Fragment, {}, renderedMergedWidget, - renderedWidgets + renderedDomainWidgets ) ) diff --git a/src/ui/scripts/utils/useWidgets.js b/src/ui/scripts/utils/useDomainWidgets.js similarity index 86% rename from src/ui/scripts/utils/useWidgets.js rename to src/ui/scripts/utils/useDomainWidgets.js index c56cb6a8..89744f89 100644 --- a/src/ui/scripts/utils/useWidgets.js +++ b/src/ui/scripts/utils/useDomainWidgets.js @@ -6,7 +6,7 @@ import { initialSubState } from '../reducers/widgets' import * as selectDomainsValue from '../selectors/selectDomainsValue' import overviewRoute from './overviewRoute' -export default (props, createLoader, opts) => { +export default (props, createLoader, opts, additionalProps = {}) => { const [ widgetIds, setWidgetIds ] = useState([]) @@ -34,7 +34,8 @@ export default (props, createLoader, opts) => { key: domain.id, headline: domain.title, widget: widgetData, - onMore: () => props.setRoute(overviewRoute(domain)) + onMore: () => props.setRoute(overviewRoute(domain)), + ...additionalProps }) } ) From 441a92ff6b3262ba67cc97951515fef931dfbeaa Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 14:12:12 +0100 Subject: [PATCH 065/208] Fix names --- src/ui/scripts/components/renderers/RendererList.js | 6 +++--- src/ui/scripts/components/renderers/RendererReferrers.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/scripts/components/renderers/RendererList.js b/src/ui/scripts/components/renderers/RendererList.js index 27b68fca..5a13d73a 100644 --- a/src/ui/scripts/components/renderers/RendererList.js +++ b/src/ui/scripts/components/renderers/RendererList.js @@ -17,7 +17,7 @@ const textLabel = (item, range, isRecent) => { } -const CardList = (props) => { +const RendererList = (props) => { const items = props.widget.value const { range, sorting } = props.widget.variables @@ -43,9 +43,9 @@ const CardList = (props) => { } -CardList.propTypes = { +RendererList.propTypes = { widget: PropTypes.object.isRequired, setTextLabel: PropTypes.func.isRequired } -export default CardList \ No newline at end of file +export default RendererList \ No newline at end of file diff --git a/src/ui/scripts/components/renderers/RendererReferrers.js b/src/ui/scripts/components/renderers/RendererReferrers.js index bc85b421..2dc3fe53 100644 --- a/src/ui/scripts/components/renderers/RendererReferrers.js +++ b/src/ui/scripts/components/renderers/RendererReferrers.js @@ -19,7 +19,7 @@ const textLabel = (item, range, isRecent, isNew) => { } -const CardReferrers = (props) => { +const RendererReferrers = (props) => { const items = props.widget.value const { range, sorting } = props.widget.variables @@ -41,9 +41,9 @@ const CardReferrers = (props) => { } -CardReferrers.propTypes = { +RendererReferrers.propTypes = { widget: PropTypes.object.isRequired, setTextLabel: PropTypes.func.isRequired } -export default CardReferrers \ No newline at end of file +export default RendererReferrers \ No newline at end of file From 183a2c5b41356b18e1c9f274dbba30c1a8787e74 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 14:15:14 +0100 Subject: [PATCH 066/208] Move hooks to customer folder --- src/ui/scripts/components/Context.js | 4 ++-- src/ui/scripts/components/routes/RouteBrowsers.js | 2 +- src/ui/scripts/components/routes/RouteDevices.js | 2 +- src/ui/scripts/components/routes/RouteDurations.js | 4 ++-- src/ui/scripts/components/routes/RouteLanguages.js | 2 +- src/ui/scripts/components/routes/RoutePages.js | 2 +- src/ui/scripts/components/routes/RouteReferrers.js | 2 +- src/ui/scripts/components/routes/RouteSizes.js | 2 +- src/ui/scripts/components/routes/RouteSystems.js | 2 +- src/ui/scripts/components/routes/RouteViews.js | 4 ++-- src/ui/scripts/{utils => hooks}/useClickAway.js | 0 src/ui/scripts/{utils => hooks}/useDomainWidgets.js | 2 +- src/ui/scripts/{utils => hooks}/useMeasure.js | 0 src/ui/scripts/{utils => hooks}/useMergedWidget.js | 0 14 files changed, 14 insertions(+), 14 deletions(-) rename src/ui/scripts/{utils => hooks}/useClickAway.js (100%) rename src/ui/scripts/{utils => hooks}/useDomainWidgets.js (95%) rename src/ui/scripts/{utils => hooks}/useMeasure.js (100%) rename src/ui/scripts/{utils => hooks}/useMergedWidget.js (100%) diff --git a/src/ui/scripts/components/Context.js b/src/ui/scripts/components/Context.js index 039b665d..0297b75d 100644 --- a/src/ui/scripts/components/Context.js +++ b/src/ui/scripts/components/Context.js @@ -3,8 +3,8 @@ import { createPortal } from 'react-dom' import PropTypes from 'prop-types' import classNames from 'classnames' -import useMeasure from '../utils/useMeasure' -import useClickAway from '../utils/useClickAway' +import useMeasure from '../hooks/useMeasure' +import useClickAway from '../hooks/useClickAway' import KeyHint from './KeyHint' diff --git a/src/ui/scripts/components/routes/RouteBrowsers.js b/src/ui/scripts/components/routes/RouteBrowsers.js index 5616246d..89f38e2c 100644 --- a/src/ui/scripts/components/routes/RouteBrowsers.js +++ b/src/ui/scripts/components/routes/RouteBrowsers.js @@ -1,7 +1,7 @@ import { createElement as h } from 'react' import browsersLoader from '../../loaders/browsersLoader' -import useDomainWidgets from '../../utils/useDomainWidgets' +import useDomainWidgets from '../../hooks/useDomainWidgets' const RouteBrowsers = (props) => { diff --git a/src/ui/scripts/components/routes/RouteDevices.js b/src/ui/scripts/components/routes/RouteDevices.js index 48a94c08..68544b80 100644 --- a/src/ui/scripts/components/routes/RouteDevices.js +++ b/src/ui/scripts/components/routes/RouteDevices.js @@ -1,7 +1,7 @@ import { createElement as h } from 'react' import devicesLoader from '../../loaders/devicesLoader' -import useDomainWidgets from '../../utils/useDomainWidgets' +import useDomainWidgets from '../../hooks/useDomainWidgets' const RouteDevices = (props) => { diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index e0c13322..39791d2f 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -2,8 +2,8 @@ import { createElement as h, Fragment } from 'react' import mergedDurationsLoader from '../../loaders/mergedDurationsLoader' import durationsLoader from '../../loaders/durationsLoader' -import useMergedWidget from '../../utils/useMergedWidget' -import useDomainWidgets from '../../utils/useDomainWidgets' +import useMergedWidget from '../../hooks/useMergedWidget' +import useDomainWidgets from '../../hooks/useDomainWidgets' const RouteDurations = (props) => { diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index 30f930ba..bb794c9c 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -1,7 +1,7 @@ import { createElement as h } from 'react' import languagesLoader from '../../loaders/languagesLoader' -import useDomainWidgets from '../../utils/useDomainWidgets' +import useDomainWidgets from '../../hooks/useDomainWidgets' const RouteLanguages = (props) => { diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index 66893b50..79f2efcc 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -1,7 +1,7 @@ import { createElement as h } from 'react' import pagesLoader from '../../loaders/pagesLoader' -import useDomainWidgets from '../../utils/useDomainWidgets' +import useDomainWidgets from '../../hooks/useDomainWidgets' const RoutePages = (props) => { diff --git a/src/ui/scripts/components/routes/RouteReferrers.js b/src/ui/scripts/components/routes/RouteReferrers.js index a59cb332..b821484f 100644 --- a/src/ui/scripts/components/routes/RouteReferrers.js +++ b/src/ui/scripts/components/routes/RouteReferrers.js @@ -1,7 +1,7 @@ import { createElement as h } from 'react' import referrersLoader from '../../loaders/referrersLoader' -import useDomainWidgets from '../../utils/useDomainWidgets' +import useDomainWidgets from '../../hooks/useDomainWidgets' const RouteReferrers = (props) => { diff --git a/src/ui/scripts/components/routes/RouteSizes.js b/src/ui/scripts/components/routes/RouteSizes.js index 9adad409..34a5d84f 100644 --- a/src/ui/scripts/components/routes/RouteSizes.js +++ b/src/ui/scripts/components/routes/RouteSizes.js @@ -1,7 +1,7 @@ import { createElement as h } from 'react' import sizesLoader from '../../loaders/sizesLoader' -import useDomainWidgets from '../../utils/useDomainWidgets' +import useDomainWidgets from '../../hooks/useDomainWidgets' const RouteSizes = (props) => { diff --git a/src/ui/scripts/components/routes/RouteSystems.js b/src/ui/scripts/components/routes/RouteSystems.js index d5a947ca..b7ca3855 100644 --- a/src/ui/scripts/components/routes/RouteSystems.js +++ b/src/ui/scripts/components/routes/RouteSystems.js @@ -1,7 +1,7 @@ import { createElement as h } from 'react' import systemsLoader from '../../loaders/systemsLoader' -import useDomainWidgets from '../../utils/useDomainWidgets' +import useDomainWidgets from '../../hooks/useDomainWidgets' const RouteSystems = (props) => { diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 08efe59b..c4b5d069 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -3,8 +3,8 @@ import { createElement as h, Fragment } from 'react' import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' import mergedViewsLoader from '../../loaders/mergedViewsLoader' import viewsLoader from '../../loaders/viewsLoader' -import useMergedWidget from '../../utils/useMergedWidget' -import useDomainWidgets from '../../utils/useDomainWidgets' +import useMergedWidget from '../../hooks/useMergedWidget' +import useDomainWidgets from '../../hooks/useDomainWidgets' const RouteViews = (props) => { diff --git a/src/ui/scripts/utils/useClickAway.js b/src/ui/scripts/hooks/useClickAway.js similarity index 100% rename from src/ui/scripts/utils/useClickAway.js rename to src/ui/scripts/hooks/useClickAway.js diff --git a/src/ui/scripts/utils/useDomainWidgets.js b/src/ui/scripts/hooks/useDomainWidgets.js similarity index 95% rename from src/ui/scripts/utils/useDomainWidgets.js rename to src/ui/scripts/hooks/useDomainWidgets.js index 89744f89..54b52aff 100644 --- a/src/ui/scripts/utils/useDomainWidgets.js +++ b/src/ui/scripts/hooks/useDomainWidgets.js @@ -4,7 +4,7 @@ import CardWidget from '../components/cards/CardWidget' import { initialSubState } from '../reducers/widgets' import * as selectDomainsValue from '../selectors/selectDomainsValue' -import overviewRoute from './overviewRoute' +import overviewRoute from '../utils/overviewRoute' export default (props, createLoader, opts, additionalProps = {}) => { diff --git a/src/ui/scripts/utils/useMeasure.js b/src/ui/scripts/hooks/useMeasure.js similarity index 100% rename from src/ui/scripts/utils/useMeasure.js rename to src/ui/scripts/hooks/useMeasure.js diff --git a/src/ui/scripts/utils/useMergedWidget.js b/src/ui/scripts/hooks/useMergedWidget.js similarity index 100% rename from src/ui/scripts/utils/useMergedWidget.js rename to src/ui/scripts/hooks/useMergedWidget.js From f5c1ade69a03eac2bb4b08d89407da20913e698a Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 14:15:24 +0100 Subject: [PATCH 067/208] Overview with merged widgets --- .../components/routes/RouteOverview.js | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/ui/scripts/components/routes/RouteOverview.js b/src/ui/scripts/components/routes/RouteOverview.js index 5937c7e5..4d60d002 100644 --- a/src/ui/scripts/components/routes/RouteOverview.js +++ b/src/ui/scripts/components/routes/RouteOverview.js @@ -2,13 +2,18 @@ import { createElement as h, Fragment, useEffect } from 'react' // import { SORTINGS_TOP } from '../../../../constants/sortings' // import { RANGES_LAST_24_HOURS } from '../../../../constants/ranges' -// import { INTERVALS_DAILY } from '../../../../constants/intervals' +import { INTERVALS_DAILY } from '../../../../constants/intervals' +import { VIEWS_TYPE_UNIQUE } from '../../../../constants/views' // import * as route from '../../constants/route' import { ALL_DOMAINS } from '../../actions/overview' import * as selectOverviewValue from '../../selectors/selectOverviewValue' // import formatNumber from '../../utils/formatNumber' // import formatDuration from '../../utils/formatDuration' +import useMergedWidget from '../../hooks/useMergedWidget' + +import mergedViewsLoader from '../../loaders/mergedViewsLoader' +import mergedDurationsLoader from '../../loaders/mergedDurationsLoader' import enhanceFacts from '../../enhancers/enhanceFacts' // import enhanceViews from '../../enhancers/enhanceViews' @@ -37,6 +42,21 @@ const RouteOverview = (props) => { }, [ domainId ]) + const renderedMergedViews = useMergedWidget(props, mergedViewsLoader, { + interval: INTERVALS_DAILY, + type: VIEWS_TYPE_UNIQUE + }, { + wide: true, + headline: () => 'Site Views' + }) + + const renderedMergedDurations = useMergedWidget(props, mergedDurationsLoader, { + interval: INTERVALS_DAILY + }, { + wide: true, + headline: () => 'Durations' + }) + return ( h(Fragment, {}, @@ -45,17 +65,10 @@ const RouteOverview = (props) => { items: enhanceFacts(selectOverviewValue.withoutType(props, domainId).facts) }), - h('div', { className: 'content__spacer' }) + h('div', { className: 'content__spacer' }), - // h(RendererChart, { - // wide: true, - // headline: 'Views', - // interval: INTERVALS_DAILY, - // loading: isLoading, - // items: enhanceViews(selectOverviewValue.withType(props, domainId, 'views'), 14), - // formatter: formatNumber, - // onMore: () => props.setRoute(route.ROUTE_VIEWS) - // }), + renderedMergedViews, + renderedMergedDurations // h(RendererChart, { // wide: true, From aec8813a629a78a9d792ba316245fcc528070155 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 17:29:06 +0100 Subject: [PATCH 068/208] Bundle requests --- src/ui/scripts/actions/widgets.js | 55 ++++++++++++++++++- src/ui/scripts/hooks/useDomainWidgets.js | 16 +++--- src/ui/scripts/loaders/browsersLoader.js | 16 +++--- src/ui/scripts/loaders/devicesLoader.js | 16 +++--- src/ui/scripts/loaders/durationsLoader.js | 14 ++--- src/ui/scripts/loaders/languagesLoader.js | 16 +++--- .../scripts/loaders/mergedDurationsLoader.js | 12 ++-- src/ui/scripts/loaders/mergedViewsLoader.js | 12 ++-- src/ui/scripts/loaders/pagesLoader.js | 16 +++--- src/ui/scripts/loaders/referrersLoader.js | 16 +++--- src/ui/scripts/loaders/sizesLoader.js | 16 +++--- src/ui/scripts/loaders/systemsLoader.js | 16 +++--- src/ui/scripts/loaders/viewsLoader.js | 14 ++--- 13 files changed, 133 insertions(+), 102 deletions(-) diff --git a/src/ui/scripts/actions/widgets.js b/src/ui/scripts/actions/widgets.js index 4f494fc7..87cef2fe 100644 --- a/src/ui/scripts/actions/widgets.js +++ b/src/ui/scripts/actions/widgets.js @@ -31,6 +31,54 @@ export const setWidgetsError = (id, payload) => ({ payload }) +export const fetchWidgets = signalHandler((signal) => (props, loaders) => async (dispatch) => { + + const id = loaders.map((loader) => loader.id).join('') + + // Generate an unique name for every query + const queryName = (index) => `_${ index }` + + // Combine multiple queries into one + const query = loaders.map((loader, index) => { + const { query } = loader + return `${ queryName(index) }: ${ query }` + }).join('') + + loaders.forEach((loader) => { + const { id, Renderer, variables } = loader + dispatch(setWidgetsStart(id, Renderer, variables)) + }) + + try { + + const data = await api({ + query: ` + { + ${ query } + } + `, + props, + signal: signal(id) + }) + + loaders.forEach((loader, index) => { + const { id, enhancer, selector } = loader + const entryName = queryName(index) + dispatch(setWidgetsEnd(id, enhancer(selector(data, entryName)))) + }) + + + } catch (err) { + + if (err.name === 'AbortError') return + loaders.forEach((loader) => dispatch(setWidgetsFetching(loader.id, false))) + if (err.name === 'HandledError') return + loaders.forEach((loader) => dispatch(setWidgetsError(loader.id, err))) + + } + +}) + export const fetchWidget = signalHandler((signal) => (props, loader) => async (dispatch) => { const { id, Renderer, query, variables, selector, enhancer } = loader @@ -40,8 +88,11 @@ export const fetchWidget = signalHandler((signal) => (props, loader) => async (d try { const data = await api({ - query, - variables, + query: ` + { + ${ query } + } + `, props, signal: signal(id) }) diff --git a/src/ui/scripts/hooks/useDomainWidgets.js b/src/ui/scripts/hooks/useDomainWidgets.js index 54b52aff..e0c78633 100644 --- a/src/ui/scripts/hooks/useDomainWidgets.js +++ b/src/ui/scripts/hooks/useDomainWidgets.js @@ -12,16 +12,18 @@ export default (props, createLoader, opts, additionalProps = {}) => { useEffect(() => { - const widgetIds = props.domains.value.map( - (domain) => { - const loader = createLoader(domain.id, opts) - props.fetchWidget(props, loader) + const loaders = props.domains.value.map((domain) => + createLoader(domain.id, opts) + ) - return loader.id - } + const widgetIds = loaders.map((loader) => + loader.id ) - setWidgetIds(widgetIds) + if (loaders.length > 0) { + props.fetchWidgets(props, loaders) + setWidgetIds(widgetIds) + } }, [ props.domains.value, ...Object.values(opts) ]) diff --git a/src/ui/scripts/loaders/browsersLoader.js b/src/ui/scripts/loaders/browsersLoader.js index 75663368..de848765 100644 --- a/src/ui/scripts/loaders/browsersLoader.js +++ b/src/ui/scripts/loaders/browsersLoader.js @@ -9,14 +9,12 @@ export default (domainId, opts) => { const id = createWidgetId('fetchBrowsers', domainId, opts) const query = ` - query fetchBrowsers($domainId: ID!, $sorting: Sorting!, $type: BrowserType!, $range: Range) { - domain(id: $domainId) { - statistics { - browsers(sorting: $sorting, type: $type, range: $range) { - id - count - created - } + domain(id: "${ domainId }") { + statistics { + browsers(sorting: ${ opts.sorting }, type: ${ opts.type }, range: ${ opts.range }) { + id + count + created } } } @@ -29,7 +27,7 @@ export default (domainId, opts) => { type: opts.type } - const selector = (data) => data.domain.statistics.browsers + const selector = (data, entryName = 'domain') => data[entryName].statistics.browsers return { id, diff --git a/src/ui/scripts/loaders/devicesLoader.js b/src/ui/scripts/loaders/devicesLoader.js index 2584d971..3b5501c9 100644 --- a/src/ui/scripts/loaders/devicesLoader.js +++ b/src/ui/scripts/loaders/devicesLoader.js @@ -9,14 +9,12 @@ export default (domainId, opts) => { const id = createWidgetId('fetchDevices', domainId, opts) const query = ` - query fetchDevices($domainId: ID!, $sorting: Sorting!, $type: DeviceType!, $range: Range) { - domain(id: $domainId) { - statistics { - devices(sorting: $sorting, type: $type, range: $range) { - id - count - created - } + domain(id: "${ domainId }") { + statistics { + devices(sorting: ${ opts.sorting }, type: ${ opts.type }, range: ${ opts.range }) { + id + count + created } } } @@ -29,7 +27,7 @@ export default (domainId, opts) => { type: opts.type } - const selector = (data) => data.domain.statistics.devices + const selector = (data, entryName = 'domain') => data[entryName].statistics.devices return { id, diff --git a/src/ui/scripts/loaders/durationsLoader.js b/src/ui/scripts/loaders/durationsLoader.js index 8fe926a4..1c3bb266 100644 --- a/src/ui/scripts/loaders/durationsLoader.js +++ b/src/ui/scripts/loaders/durationsLoader.js @@ -10,13 +10,11 @@ export default (domainId, opts) => { const id = createWidgetId('fetchDurations', domainId, opts) const query = ` - query fetchDurations($domainId: ID!, $interval: Interval!) { - domain(id: $domainId) { - statistics { - durations(interval: $interval, limit: 7) { - id - count - } + domain(id: "${ domainId }") { + statistics { + durations(interval: ${ opts.interval }, limit: 7) { + id + count } } } @@ -27,7 +25,7 @@ export default (domainId, opts) => { interval: opts.interval } - const selector = (data) => data.domain.statistics.durations + const selector = (data, entryName = 'domain') => data[entryName].statistics.durations return { id, diff --git a/src/ui/scripts/loaders/languagesLoader.js b/src/ui/scripts/loaders/languagesLoader.js index 7715e6c3..e012bf99 100644 --- a/src/ui/scripts/loaders/languagesLoader.js +++ b/src/ui/scripts/loaders/languagesLoader.js @@ -9,14 +9,12 @@ export default (domainId, opts) => { const id = createWidgetId('fetchLanguages', domainId, opts) const query = ` - query fetchLanguages($domainId: ID!, $sorting: Sorting!, $range: Range) { - domain(id: $domainId) { - statistics { - languages(sorting: $sorting, range: $range) { - id - count - created - } + domain(id: "${ domainId }") { + statistics { + languages(sorting: ${ opts.sorting }, range: ${ opts.range }) { + id + count + created } } } @@ -28,7 +26,7 @@ export default (domainId, opts) => { range: opts.range } - const selector = (data) => data.domain.statistics.languages + const selector = (data, entryName = 'domain') => data[entryName].domain.statistics.languages return { id, diff --git a/src/ui/scripts/loaders/mergedDurationsLoader.js b/src/ui/scripts/loaders/mergedDurationsLoader.js index b993c6f2..2783337b 100644 --- a/src/ui/scripts/loaders/mergedDurationsLoader.js +++ b/src/ui/scripts/loaders/mergedDurationsLoader.js @@ -10,12 +10,10 @@ export default (opts) => { const id = createWidgetId('fetchMergedDurations', undefined, opts) const query = ` - query fetchMergedDurations($interval: Interval!) { - statistics { - durations(interval: $interval) { - id - count - } + statistics { + durations(interval: ${ opts.interval }) { + id + count } } ` @@ -24,7 +22,7 @@ export default (opts) => { interval: opts.interval } - const selector = (data) => data.statistics.durations + const selector = (data, entryName = 'statistics') => data[entryName].durations return { id, diff --git a/src/ui/scripts/loaders/mergedViewsLoader.js b/src/ui/scripts/loaders/mergedViewsLoader.js index 9a538c95..f4b0fd77 100644 --- a/src/ui/scripts/loaders/mergedViewsLoader.js +++ b/src/ui/scripts/loaders/mergedViewsLoader.js @@ -10,12 +10,10 @@ export default (opts) => { const id = createWidgetId('fetchMergedViews', undefined, opts) const query = ` - query fetchMergedViews($interval: Interval!, $type: ViewType!) { - statistics { - views(interval: $interval, type: $type) { - id - count - } + statistics { + views(interval: ${ opts.interval }, type: ${ opts.type }) { + id + count } } ` @@ -25,7 +23,7 @@ export default (opts) => { type: opts.type } - const selector = (data) => data.statistics.views + const selector = (data, entryName = 'statistics') => data[entryName].views return { id, diff --git a/src/ui/scripts/loaders/pagesLoader.js b/src/ui/scripts/loaders/pagesLoader.js index 95962887..c564e69c 100644 --- a/src/ui/scripts/loaders/pagesLoader.js +++ b/src/ui/scripts/loaders/pagesLoader.js @@ -9,14 +9,12 @@ export default (domainId, opts) => { const id = createWidgetId('fetchPages', domainId, opts) const query = ` - query fetchPages($domainId: ID!, $sorting: Sorting!, $range: Range) { - domain(id: $domainId) { - statistics { - pages(sorting: $sorting, range: $range) { - id - count - created - } + domain(id: "${ domainId }") { + statistics { + pages(sorting: ${ opts.sorting }, range: ${ opts.range }) { + id + count + created } } } @@ -28,7 +26,7 @@ export default (domainId, opts) => { range: opts.range } - const selector = (data) => data.domain.statistics.pages + const selector = (data, entryName = 'domain') => data[entryName].statistics.pages return { id, diff --git a/src/ui/scripts/loaders/referrersLoader.js b/src/ui/scripts/loaders/referrersLoader.js index f0ad79d9..3ca287d2 100644 --- a/src/ui/scripts/loaders/referrersLoader.js +++ b/src/ui/scripts/loaders/referrersLoader.js @@ -9,14 +9,12 @@ export default (domainId, opts) => { const id = createWidgetId('fetchReferrers', domainId, opts) const query = ` - query fetchReferrers($domainId: ID!, $sorting: Sorting!, $range: Range) { - domain(id: $domainId) { - statistics { - referrers(sorting: $sorting, range: $range) { - id - count - created - } + domain(id: "${ domainId }") { + statistics { + referrers(sorting: ${ opts.sorting }, range: ${ opts.range }) { + id + count + created } } } @@ -28,7 +26,7 @@ export default (domainId, opts) => { range: opts.range } - const selector = (data) => data.domain.statistics.referrers + const selector = (data, entryName = 'domain') => data[entryName].statistics.referrers return { id, diff --git a/src/ui/scripts/loaders/sizesLoader.js b/src/ui/scripts/loaders/sizesLoader.js index 6250daff..8280438d 100644 --- a/src/ui/scripts/loaders/sizesLoader.js +++ b/src/ui/scripts/loaders/sizesLoader.js @@ -9,14 +9,12 @@ export default (domainId, opts) => { const id = createWidgetId('fetchSizes', domainId, opts) const query = ` - query fetchSizes($domainId: ID!, $sorting: Sorting!, $type: SizeType!, $range: Range) { - domain(id: $domainId) { - statistics { - sizes(sorting: $sorting, type: $type, range: $range) { - id - count - created - } + domain(id: "${ domainId }") { + statistics { + sizes(sorting: ${ opts.sorting }, type: ${ opts.type }, range: ${ opts.range }) { + id + count + created } } } @@ -29,7 +27,7 @@ export default (domainId, opts) => { type: opts.type } - const selector = (data) => data.domain.statistics.sizes + const selector = (data, entryName = 'domain') => data[entryName].statistics.sizes return { id, diff --git a/src/ui/scripts/loaders/systemsLoader.js b/src/ui/scripts/loaders/systemsLoader.js index 47c32224..09fa832c 100644 --- a/src/ui/scripts/loaders/systemsLoader.js +++ b/src/ui/scripts/loaders/systemsLoader.js @@ -9,14 +9,12 @@ export default (domainId, opts) => { const id = createWidgetId('fetchSystems', domainId, opts) const query = ` - query fetchSystems($domainId: ID!, $sorting: Sorting!, $type: SystemType!, $range: Range) { - domain(id: $domainId) { - statistics { - systems(sorting: $sorting, type: $type, range: $range) { - id - count - created - } + domain(id: "${ domainId }") { + statistics { + systems(sorting: ${ opts.sorting }, type: ${ opts.type }, range: ${ opts.range }) { + id + count + created } } } @@ -29,7 +27,7 @@ export default (domainId, opts) => { type: opts.type } - const selector = (data) => data.domain.statistics.systems + const selector = (data, entryName = 'domain') => data[entryName].statistics.systems return { id, diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js index e3a34d30..2868b83e 100644 --- a/src/ui/scripts/loaders/viewsLoader.js +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -10,13 +10,11 @@ export default (domainId, opts) => { const id = createWidgetId('fetchViews', domainId, opts) const query = ` - query fetchViews($domainId: ID!, $interval: Interval!, $type: ViewType!) { - domain(id: $domainId) { - statistics { - views(interval: $interval, type: $type, limit: 7) { - id - count - } + domain(id: "${ domainId }") { + statistics { + views(interval: ${ opts.interval }, type: ${ opts.type }, limit: 7) { + id + count } } } @@ -28,7 +26,7 @@ export default (domainId, opts) => { type: opts.type } - const selector = (data) => data.domain.statistics.views + const selector = (data, entryName = 'domain') => data[entryName].statistics.views return { id, From b371955ebca4275d01d89fe6e76aa0c0a19881c3 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 20:29:34 +0100 Subject: [PATCH 069/208] Fix selector --- src/ui/scripts/loaders/languagesLoader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/scripts/loaders/languagesLoader.js b/src/ui/scripts/loaders/languagesLoader.js index e012bf99..0a7b4c7c 100644 --- a/src/ui/scripts/loaders/languagesLoader.js +++ b/src/ui/scripts/loaders/languagesLoader.js @@ -26,7 +26,7 @@ export default (domainId, opts) => { range: opts.range } - const selector = (data, entryName = 'domain') => data[entryName].domain.statistics.languages + const selector = (data, entryName = 'domain') => data[entryName].statistics.languages return { id, From 643e42149a72e5b511fc719cc39dc29d887326ac Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 6 Dec 2020 21:11:49 +0100 Subject: [PATCH 070/208] Refactor overview --- .../components/routes/RouteOverview.js | 235 +++++++++--------- src/ui/scripts/hooks/useWidgets.js | 37 +++ src/ui/scripts/loaders/browsersLoader.js | 4 +- src/ui/scripts/loaders/devicesLoader.js | 4 +- .../scripts/loaders/mergedBrowsersLoader.js | 38 +++ src/ui/scripts/loaders/mergedDevicesLoader.js | 38 +++ .../scripts/loaders/mergedLanguagesLoader.js | 37 +++ src/ui/scripts/loaders/mergedPagesLoader.js | 37 +++ .../scripts/loaders/mergedReferrersLoader.js | 37 +++ src/ui/scripts/loaders/mergedSizesLoader.js | 38 +++ src/ui/scripts/loaders/mergedSystemsLoader.js | 38 +++ src/ui/scripts/loaders/sizesLoader.js | 4 +- src/ui/scripts/loaders/systemsLoader.js | 4 +- src/ui/scripts/loaders/viewsLoader.js | 4 +- 14 files changed, 430 insertions(+), 125 deletions(-) create mode 100644 src/ui/scripts/hooks/useWidgets.js create mode 100644 src/ui/scripts/loaders/mergedBrowsersLoader.js create mode 100644 src/ui/scripts/loaders/mergedDevicesLoader.js create mode 100644 src/ui/scripts/loaders/mergedLanguagesLoader.js create mode 100644 src/ui/scripts/loaders/mergedPagesLoader.js create mode 100644 src/ui/scripts/loaders/mergedReferrersLoader.js create mode 100644 src/ui/scripts/loaders/mergedSizesLoader.js create mode 100644 src/ui/scripts/loaders/mergedSystemsLoader.js diff --git a/src/ui/scripts/components/routes/RouteOverview.js b/src/ui/scripts/components/routes/RouteOverview.js index 4d60d002..407ff799 100644 --- a/src/ui/scripts/components/routes/RouteOverview.js +++ b/src/ui/scripts/components/routes/RouteOverview.js @@ -1,35 +1,32 @@ import { createElement as h, Fragment, useEffect } from 'react' -// import { SORTINGS_TOP } from '../../../../constants/sortings' -// import { RANGES_LAST_24_HOURS } from '../../../../constants/ranges' +import { SORTINGS_TOP } from '../../../../constants/sortings' +import { RANGES_LAST_24_HOURS } from '../../../../constants/ranges' import { INTERVALS_DAILY } from '../../../../constants/intervals' import { VIEWS_TYPE_UNIQUE } from '../../../../constants/views' +import { SYSTEMS_TYPE_WITH_VERSION } from '../../../../constants/systems' +import { DEVICES_TYPE_WITH_MODEL } from '../../../../constants/devices' +import { BROWSERS_TYPE_WITH_VERSION } from '../../../../constants/browsers' +import { SIZES_TYPE_BROWSER_RESOLUTION } from '../../../../constants/sizes' -// import * as route from '../../constants/route' +import * as route from '../../constants/route' import { ALL_DOMAINS } from '../../actions/overview' import * as selectOverviewValue from '../../selectors/selectOverviewValue' -// import formatNumber from '../../utils/formatNumber' -// import formatDuration from '../../utils/formatDuration' -import useMergedWidget from '../../hooks/useMergedWidget' +import useWidgets from '../../hooks/useWidgets' import mergedViewsLoader from '../../loaders/mergedViewsLoader' import mergedDurationsLoader from '../../loaders/mergedDurationsLoader' - +import mergedPagesLoader from '../../loaders/mergedPagesLoader' +import mergedReferrersLoader from '../../loaders/mergedReferrersLoader' +import mergedSystemsLoader from '../../loaders/mergedSystemsLoader' +import mergedDevicesLoader from '../../loaders/mergedDevicesLoader' +import mergedBrowsersLoader from '../../loaders/mergedBrowsersLoader' +import mergedSizesLoader from '../../loaders/mergedSizesLoader' +import mergedLanguagesLoader from '../../loaders/mergedLanguagesLoader' + +// TODO: Refactor facts import enhanceFacts from '../../enhancers/enhanceFacts' -// import enhanceViews from '../../enhancers/enhanceViews' -// import enhancePages from '../../enhancers/enhancePages' -// import enhanceReferrers from '../../enhancers/enhanceReferrers' -// import enhanceDurations from '../../enhancers/enhanceDurations' -// import enhanceSystems from '../../enhancers/enhanceSystems' -// import enhanceDevices from '../../enhancers/enhanceDevices' -// import enhanceBrowsers from '../../enhancers/enhanceBrowsers' -// import enhanceSizes from '../../enhancers/enhanceSizes' -// import enhanceLanguages from '../../enhancers/enhanceLanguages' - import CardFacts from '../cards/CardFacts' -// import RendererChart from '../renderers/RendererChart' -// import RendererReferrers from '../renderers/RendererReferrers' -// import RendererList from '../renderers/RendererList' const RouteOverview = (props) => { @@ -42,109 +39,117 @@ const RouteOverview = (props) => { }, [ domainId ]) - const renderedMergedViews = useMergedWidget(props, mergedViewsLoader, { - interval: INTERVALS_DAILY, - type: VIEWS_TYPE_UNIQUE - }, { - wide: true, - headline: () => 'Site Views' - }) - - const renderedMergedDurations = useMergedWidget(props, mergedDurationsLoader, { - interval: INTERVALS_DAILY - }, { - wide: true, - headline: () => 'Durations' - }) + const renderedEssentialWidgets = useWidgets(props, [ + { + loader: mergedViewsLoader({ + interval: INTERVALS_DAILY, + type: VIEWS_TYPE_UNIQUE + }), + additionalProps: { + wide: true, + headline: 'Site Views', + onMore: () => props.setRoute(route.ROUTE_VIEWS) + } + }, + { + loader: mergedDurationsLoader({ + interval: INTERVALS_DAILY + }), + additionalProps: { + wide: true, + headline: 'Durations', + onMore: () => props.setRoute(route.ROUTE_DURATIONS) + } + }, + { + loader: mergedPagesLoader({ + range: RANGES_LAST_24_HOURS, + sorting: SORTINGS_TOP + }), + additionalProps: { + headline: 'Pages', + onMore: () => props.setRoute(route.ROUTE_PAGES) + } + }, + { + loader: mergedReferrersLoader({ + range: RANGES_LAST_24_HOURS, + sorting: SORTINGS_TOP + }), + additionalProps: { + headline: 'Referrers', + onMore: () => props.setRoute(route.ROUTE_REFERRERS) + } + } + ]) + + const renderedDetailedWidgets = useWidgets(props, [ + { + loader: mergedSystemsLoader({ + sorting: SORTINGS_TOP, + range: RANGES_LAST_24_HOURS, + type: SYSTEMS_TYPE_WITH_VERSION + }), + additionalProps: { + headline: 'Systems', + onMore: () => props.setRoute(route.ROUTE_SYSTEMS) + } + }, + { + loader: mergedDevicesLoader({ + sorting: SORTINGS_TOP, + range: RANGES_LAST_24_HOURS, + type: DEVICES_TYPE_WITH_MODEL + }), + additionalProps: { + headline: 'Devices', + onMore: () => props.setRoute(route.ROUTE_DEVICES) + } + }, + { + loader: mergedBrowsersLoader({ + sorting: SORTINGS_TOP, + range: RANGES_LAST_24_HOURS, + type: BROWSERS_TYPE_WITH_VERSION + }), + additionalProps: { + headline: 'Browsers', + onMore: () => props.setRoute(route.ROUTE_BROWSERS) + } + }, + { + loader: mergedSizesLoader({ + sorting: SORTINGS_TOP, + range: RANGES_LAST_24_HOURS, + type: SIZES_TYPE_BROWSER_RESOLUTION + }), + additionalProps: { + headline: 'Sizes', + onMore: () => props.setRoute(route.ROUTE_SIZES) + } + }, + { + loader: mergedLanguagesLoader({ + sorting: SORTINGS_TOP, + range: RANGES_LAST_24_HOURS + }), + additionalProps: { + headline: 'Languages', + onMore: () => props.setRoute(route.ROUTE_LANGUAGES) + } + } + ]) return ( h(Fragment, {}, - h(CardFacts, { loading: isLoading, items: enhanceFacts(selectOverviewValue.withoutType(props, domainId).facts) }), - h('div', { className: 'content__spacer' }), - - renderedMergedViews, - renderedMergedDurations - - // h(RendererChart, { - // wide: true, - // headline: 'Durations', - // interval: INTERVALS_DAILY, - // loading: isLoading, - // items: enhanceDurations(selectOverviewValue.withType(props, domainId, 'durations'), 14), - // formatter: (ms) => formatDuration(ms).toString(), - // onMore: () => props.setRoute(route.ROUTE_DURATIONS) - // }), - - // h(RendererList, { - // headline: 'Pages', - // range: RANGES_LAST_24_HOURS, - // sorting: SORTINGS_TOP, - // loading: isLoading, - // items: enhancePages(selectOverviewValue.withType(props, domainId, 'pages')), - // onMore: () => props.setRoute(route.ROUTE_PAGES) - // }), - - // h(RendererReferrers, { - // headline: 'Referrers', - // range: RANGES_LAST_24_HOURS, - // sorting: SORTINGS_TOP, - // loading: isLoading, - // items: enhanceReferrers(selectOverviewValue.withType(props, domainId, 'referrers')), - // onMore: () => props.setRoute(route.ROUTE_REFERRERS) - // }), - - // h('div', { className: 'content__spacer' }), - - // h(RendererList, { - // headline: 'Systems', - // range: RANGES_LAST_24_HOURS, - // sorting: SORTINGS_TOP, - // loading: isLoading, - // items: enhanceSystems(selectOverviewValue.withType(props, domainId, 'systems')), - // onMore: () => props.setRoute(route.ROUTE_SYSTEMS) - // }), - - // h(RendererList, { - // headline: 'Devices', - // range: RANGES_LAST_24_HOURS, - // sorting: SORTINGS_TOP, - // loading: isLoading, - // items: enhanceDevices(selectOverviewValue.withType(props, domainId, 'devices')), - // onMore: () => props.setRoute(route.ROUTE_DEVICES) - // }), - - // h(RendererList, { - // headline: 'Browsers', - // range: RANGES_LAST_24_HOURS, - // sorting: SORTINGS_TOP, - // loading: isLoading, - // items: enhanceBrowsers(selectOverviewValue.withType(props, domainId, 'browsers')), - // onMore: () => props.setRoute(route.ROUTE_BROWSERS) - // }), - - // h(RendererList, { - // headline: 'Sizes', - // range: RANGES_LAST_24_HOURS, - // sorting: SORTINGS_TOP, - // loading: isLoading, - // items: enhanceSizes(selectOverviewValue.withType(props, domainId, 'sizes')), - // onMore: () => props.setRoute(route.ROUTE_SIZES) - // }), - - // h(RendererList, { - // headline: 'Languages', - // range: RANGES_LAST_24_HOURS, - // sorting: SORTINGS_TOP, - // loading: isLoading, - // items: enhanceLanguages(selectOverviewValue.withType(props, domainId, 'languages')), - // onMore: () => props.setRoute(route.ROUTE_LANGUAGES) - // }) - + renderedEssentialWidgets, + h('div', { className: 'content__spacer' }), + renderedDetailedWidgets ) ) diff --git a/src/ui/scripts/hooks/useWidgets.js b/src/ui/scripts/hooks/useWidgets.js new file mode 100644 index 00000000..10e08342 --- /dev/null +++ b/src/ui/scripts/hooks/useWidgets.js @@ -0,0 +1,37 @@ +import { createElement as h, useMemo } from 'react' + +import { initialSubState } from '../reducers/widgets' + +import CardWidget from '../components/cards/CardWidget' + +export default (props, widgetConfigs = []) => { + + const widgetIds = useMemo(() => { + + const loaders = widgetConfigs.map((widgetConfig) => + widgetConfig.loader + ) + + const widgetIds = loaders.map((loader) => + loader.id + ) + + props.fetchWidgets(props, loaders) + return widgetIds + + // TODO: Rerender when widgetConfigs changes + }, []) + + return widgetIds.map( + (widgetId, index) => { + const widgetData = props.widgets.value[widgetId] || initialSubState() + + return h(CardWidget, { + key: widgetId, + widget: widgetData, + ...widgetConfigs[index].additionalProps + }) + } + ) + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/browsersLoader.js b/src/ui/scripts/loaders/browsersLoader.js index de848765..1307fbd8 100644 --- a/src/ui/scripts/loaders/browsersLoader.js +++ b/src/ui/scripts/loaders/browsersLoader.js @@ -23,8 +23,8 @@ export default (domainId, opts) => { const variables = { domainId, sorting: opts.sorting, - range: opts.range, - type: opts.type + type: opts.type, + range: opts.range } const selector = (data, entryName = 'domain') => data[entryName].statistics.browsers diff --git a/src/ui/scripts/loaders/devicesLoader.js b/src/ui/scripts/loaders/devicesLoader.js index 3b5501c9..6a1af503 100644 --- a/src/ui/scripts/loaders/devicesLoader.js +++ b/src/ui/scripts/loaders/devicesLoader.js @@ -23,8 +23,8 @@ export default (domainId, opts) => { const variables = { domainId, sorting: opts.sorting, - range: opts.range, - type: opts.type + type: opts.type, + range: opts.range } const selector = (data, entryName = 'domain') => data[entryName].statistics.devices diff --git a/src/ui/scripts/loaders/mergedBrowsersLoader.js b/src/ui/scripts/loaders/mergedBrowsersLoader.js new file mode 100644 index 00000000..81c43d85 --- /dev/null +++ b/src/ui/scripts/loaders/mergedBrowsersLoader.js @@ -0,0 +1,38 @@ +import { createElement as h } from 'react' + +import RendererList from '../components/renderers/RendererList' +import enhanceBrowsers from '../enhancers/enhanceBrowsers' +import createWidgetId from '../utils/createWidgetId' + +export default (opts) => { + + const id = createWidgetId('fetchMergedBrowsers', undefined, opts) + + const query = ` + statistics { + browsers(sorting: ${ opts.sorting }, type: ${ opts.type }, range: ${ opts.range }) { + id + count + created + } + } + ` + + const variables = { + sorting: opts.sorting, + type: opts.type, + range: opts.range + } + + const selector = (data, entryName = 'statistics') => data[entryName].browsers + + return { + id, + Renderer: RendererList, + query, + variables, + selector, + enhancer: enhanceBrowsers + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/mergedDevicesLoader.js b/src/ui/scripts/loaders/mergedDevicesLoader.js new file mode 100644 index 00000000..2c74bc27 --- /dev/null +++ b/src/ui/scripts/loaders/mergedDevicesLoader.js @@ -0,0 +1,38 @@ +import { createElement as h } from 'react' + +import RendererList from '../components/renderers/RendererList' +import enhanceDevices from '../enhancers/enhanceDevices' +import createWidgetId from '../utils/createWidgetId' + +export default (opts) => { + + const id = createWidgetId('fetchMergedDevices', undefined, opts) + + const query = ` + statistics { + devices(sorting: ${ opts.sorting }, type: ${ opts.type }, range: ${ opts.range }) { + id + count + created + } + } + ` + + const variables = { + sorting: opts.sorting, + type: opts.type, + range: opts.range + } + + const selector = (data, entryName = 'statistics') => data[entryName].devices + + return { + id, + Renderer: RendererList, + query, + variables, + selector, + enhancer: enhanceDevices + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/mergedLanguagesLoader.js b/src/ui/scripts/loaders/mergedLanguagesLoader.js new file mode 100644 index 00000000..feb2fa24 --- /dev/null +++ b/src/ui/scripts/loaders/mergedLanguagesLoader.js @@ -0,0 +1,37 @@ +import { createElement as h } from 'react' + +import RendererList from '../components/renderers/RendererList' +import enhanceLanguages from '../enhancers/enhanceLanguages' +import createWidgetId from '../utils/createWidgetId' + +export default (opts) => { + + const id = createWidgetId('fetchLanguages', undefined, opts) + + const query = ` + statistics { + languages(sorting: ${ opts.sorting }, range: ${ opts.range }) { + id + count + created + } + } + ` + + const variables = { + sorting: opts.sorting, + range: opts.range + } + + const selector = (data, entryName = 'statistics') => data[entryName].languages + + return { + id, + Renderer: RendererList, + query, + variables, + selector, + enhancer: enhanceLanguages + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/mergedPagesLoader.js b/src/ui/scripts/loaders/mergedPagesLoader.js new file mode 100644 index 00000000..240433c5 --- /dev/null +++ b/src/ui/scripts/loaders/mergedPagesLoader.js @@ -0,0 +1,37 @@ +import { createElement as h } from 'react' + +import RendererList from '../components/renderers/RendererList' +import enhancePages from '../enhancers/enhancePages' +import createWidgetId from '../utils/createWidgetId' + +export default (opts) => { + + const id = createWidgetId('fetchPages', undefined, opts) + + const query = ` + statistics { + pages(sorting: ${ opts.sorting }, range: ${ opts.range }) { + id + count + created + } + } + ` + + const variables = { + sorting: opts.sorting, + range: opts.range + } + + const selector = (data, entryName = 'statistics') => data[entryName].pages + + return { + id, + Renderer: RendererList, + query, + variables, + selector, + enhancer: enhancePages + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/mergedReferrersLoader.js b/src/ui/scripts/loaders/mergedReferrersLoader.js new file mode 100644 index 00000000..16cbcfcd --- /dev/null +++ b/src/ui/scripts/loaders/mergedReferrersLoader.js @@ -0,0 +1,37 @@ +import { createElement as h } from 'react' + +import RendererReferrers from '../components/renderers/RendererReferrers' +import enhanceReferrers from '../enhancers/enhanceReferrers' +import createWidgetId from '../utils/createWidgetId' + +export default (opts) => { + + const id = createWidgetId('fetchReferrers', undefined, opts) + + const query = ` + statistics { + referrers(sorting: ${ opts.sorting }, range: ${ opts.range }) { + id + count + created + } + } + ` + + const variables = { + sorting: opts.sorting, + range: opts.range + } + + const selector = (data, entryName = 'statistics') => data[entryName].referrers + + return { + id, + Renderer: RendererReferrers, + query, + variables, + selector, + enhancer: enhanceReferrers + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/mergedSizesLoader.js b/src/ui/scripts/loaders/mergedSizesLoader.js new file mode 100644 index 00000000..749daba4 --- /dev/null +++ b/src/ui/scripts/loaders/mergedSizesLoader.js @@ -0,0 +1,38 @@ +import { createElement as h } from 'react' + +import RendererList from '../components/renderers/RendererList' +import enhanceSizes from '../enhancers/enhanceSizes' +import createWidgetId from '../utils/createWidgetId' + +export default (opts) => { + + const id = createWidgetId('fetchSizes', undefined, opts) + + const query = ` + statistics { + sizes(sorting: ${ opts.sorting }, type: ${ opts.type }, range: ${ opts.range }) { + id + count + created + } + } + ` + + const variables = { + sorting: opts.sorting, + type: opts.type, + range: opts.range + } + + const selector = (data, entryName = 'statistics') => data[entryName].sizes + + return { + id, + Renderer: RendererList, + query, + variables, + selector, + enhancer: enhanceSizes + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/mergedSystemsLoader.js b/src/ui/scripts/loaders/mergedSystemsLoader.js new file mode 100644 index 00000000..098ebdd2 --- /dev/null +++ b/src/ui/scripts/loaders/mergedSystemsLoader.js @@ -0,0 +1,38 @@ +import { createElement as h } from 'react' + +import RendererList from '../components/renderers/RendererList' +import enhanceSystems from '../enhancers/enhanceSystems' +import createWidgetId from '../utils/createWidgetId' + +export default (opts) => { + + const id = createWidgetId('fetchSystems', undefined, opts) + + const query = ` + statistics { + systems(sorting: ${ opts.sorting }, type: ${ opts.type }, range: ${ opts.range }) { + id + count + created + } + } + ` + + const variables = { + sorting: opts.sorting, + type: opts.type, + range: opts.range + } + + const selector = (data, entryName = 'statistics') => data[entryName].systems + + return { + id, + Renderer: RendererList, + query, + variables, + selector, + enhancer: enhanceSystems + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/sizesLoader.js b/src/ui/scripts/loaders/sizesLoader.js index 8280438d..a660c425 100644 --- a/src/ui/scripts/loaders/sizesLoader.js +++ b/src/ui/scripts/loaders/sizesLoader.js @@ -23,8 +23,8 @@ export default (domainId, opts) => { const variables = { domainId, sorting: opts.sorting, - range: opts.range, - type: opts.type + type: opts.type, + range: opts.range } const selector = (data, entryName = 'domain') => data[entryName].statistics.sizes diff --git a/src/ui/scripts/loaders/systemsLoader.js b/src/ui/scripts/loaders/systemsLoader.js index 09fa832c..d95f8ca0 100644 --- a/src/ui/scripts/loaders/systemsLoader.js +++ b/src/ui/scripts/loaders/systemsLoader.js @@ -23,8 +23,8 @@ export default (domainId, opts) => { const variables = { domainId, sorting: opts.sorting, - range: opts.range, - type: opts.type + type: opts.type, + range: opts.range } const selector = (data, entryName = 'domain') => data[entryName].statistics.systems diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js index 2868b83e..4be51dcd 100644 --- a/src/ui/scripts/loaders/viewsLoader.js +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -22,8 +22,8 @@ export default (domainId, opts) => { const variables = { domainId, - interval: opts.interval, - type: opts.type + type: opts.type, + interval: opts.interval } const selector = (data, entryName = 'domain') => data[entryName].statistics.views From 122a3de80e91cd2cae5954eb2e41811318e970fe Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 14:25:37 +0100 Subject: [PATCH 071/208] Use uuid v5 to generate ids --- src/ui/scripts/utils/createWidgetId.js | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/ui/scripts/utils/createWidgetId.js b/src/ui/scripts/utils/createWidgetId.js index e3be7de3..c269c199 100644 --- a/src/ui/scripts/utils/createWidgetId.js +++ b/src/ui/scripts/utils/createWidgetId.js @@ -1,23 +1,10 @@ -import { v4 as uuid } from 'uuid' - -const ids = new Map() - -const existingId = (key) => { - return ids.get(key) -} - -const newId = (key) => { - const id = uuid() - ids.set(key, id) - return id -} +import { v5 as uuid } from 'uuid' export default (type, domainId, opts) => { - const key = `${ type }${ domainId || '' }${ JSON.stringify(opts) }` - const id = existingId(key) + const name = `${ type }${ domainId || '' }${ JSON.stringify(opts) }` + const namespace = '906d7dd0-b6e0-42c8-9270-436958c29c36' - if (id == null) return newId(key) - return id + return uuid(name, namespace) } \ No newline at end of file From 950373dc660aecbdeddfd3b2b2bcbfd374969345 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 14:41:15 +0100 Subject: [PATCH 072/208] Save all filters --- src/ui/scripts/index.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ui/scripts/index.js b/src/ui/scripts/index.js index 9904a6c8..36ad7b3b 100644 --- a/src/ui/scripts/index.js +++ b/src/ui/scripts/index.js @@ -46,12 +46,7 @@ store.subscribe(() => { }, filter: { ...initialFilterState(), - sorting: currentState.filter.sorting, - range: currentState.filter.range, - interval: currentState.filter.interval, - viewsType: currentState.filter.viewsType, - devicesType: currentState.filter.devicesType, - browsersType: currentState.filter.browsersType + ...currentState.filter } }) From 60849990d1bcfe763c42c207da93dd21eaa8e123 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 14:41:34 +0100 Subject: [PATCH 073/208] Remove unused loading prop --- src/ui/scripts/components/cards/CardFacts.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ui/scripts/components/cards/CardFacts.js b/src/ui/scripts/components/cards/CardFacts.js index fd8662d9..dc1df870 100644 --- a/src/ui/scripts/components/cards/CardFacts.js +++ b/src/ui/scripts/components/cards/CardFacts.js @@ -67,7 +67,6 @@ const CardFacts = (props) => { } CardFacts.propTypes = { - loading: PropTypes.bool.isRequired, items: PropTypes.exact({ activeVisitors: PropTypes.number.isRequired, averageViews: PropTypes.number.isRequired, From d2bb2b0b6003ab1e71161717bfd01f4c2a5d633d Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 14:42:08 +0100 Subject: [PATCH 074/208] Remove unused files --- src/ui/scripts/utils/genericState.js | 5 ---- src/ui/scripts/utils/mergeDurations.js | 40 -------------------------- src/ui/scripts/utils/mergeViews.js | 31 -------------------- 3 files changed, 76 deletions(-) delete mode 100644 src/ui/scripts/utils/genericState.js delete mode 100644 src/ui/scripts/utils/mergeDurations.js delete mode 100644 src/ui/scripts/utils/mergeViews.js diff --git a/src/ui/scripts/utils/genericState.js b/src/ui/scripts/utils/genericState.js deleted file mode 100644 index 62776221..00000000 --- a/src/ui/scripts/utils/genericState.js +++ /dev/null @@ -1,5 +0,0 @@ -export default () => ({ - value: {}, - fetching: false, - error: undefined -}) \ No newline at end of file diff --git a/src/ui/scripts/utils/mergeDurations.js b/src/ui/scripts/utils/mergeDurations.js deleted file mode 100644 index 4264c43e..00000000 --- a/src/ui/scripts/utils/mergeDurations.js +++ /dev/null @@ -1,40 +0,0 @@ -import enhanceDurations from '../enhancers/enhanceDurations' - -// Turns the durations of multiple widgets into one array of durations -export default (widgets) => { - - // Enhance durations for all widgets - const enhancedDurations = widgets.map((widget) => { - - return enhanceDurations(widget.value, 14) - - }) - - // Merge all durations to one array of durations - const mergedDurations = enhancedDurations.reduce((acc, durations) => { - - // Durations is an array. Each item represents the average duration of one day. - durations.forEach((duration, index) => { - - // The current day might be new as should be initialised first - const initial = acc[index] == null ? 0 : acc[index] - - // Add the current day to the global array of days - acc[index] = initial + duration - - }) - - return acc - - }, []) - - const totalDomains = enhancedDurations.length - - // Convert merged, total durations into average durations - return mergedDurations.map((duration) => { - - return duration / totalDomains - - }) - -} \ No newline at end of file diff --git a/src/ui/scripts/utils/mergeViews.js b/src/ui/scripts/utils/mergeViews.js deleted file mode 100644 index 66998be1..00000000 --- a/src/ui/scripts/utils/mergeViews.js +++ /dev/null @@ -1,31 +0,0 @@ -import enhanceViews from '../enhancers/enhanceViews' - -// Turns the views of multiple widgets into one array of views -export default (widgets) => { - - // Enhance views for all widgets - const enhancedViews = widgets.map((widget) => { - - return enhanceViews(widget.value, 14) - - }) - - // Merge all views to one array of views - return enhancedViews.reduce((acc, views) => { - - // Views is an array. Each item represents the visit count of one day, month or year. - views.forEach((view, index) => { - - // The current day, month or year might be new and should be initialised first - const initial = acc[index] == null ? 0 : acc[index] - - // Add the current day, month or year to the global array of days, months or years - acc[index] = initial + view - - }) - - return acc - - }, []) - -} \ No newline at end of file From ffcca938a8b8bb5b01bb0f4e48d0bcba2492e015 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 14:42:25 +0100 Subject: [PATCH 075/208] Remove overview redux state and related files --- src/ui/scripts/actions/index.js | 1 - src/ui/scripts/actions/overview.js | 158 ------------------ .../components/routes/RouteOverview.js | 24 +-- src/ui/scripts/enhancers/enhanceState.js | 6 - src/ui/scripts/reducers/index.js | 2 - src/ui/scripts/reducers/overview.js | 46 ----- .../scripts/selectors/selectOverviewValue.js | 12 -- src/ui/scripts/utils/genericSubState.js | 3 - 8 files changed, 7 insertions(+), 245 deletions(-) delete mode 100644 src/ui/scripts/actions/overview.js delete mode 100644 src/ui/scripts/reducers/overview.js delete mode 100644 src/ui/scripts/selectors/selectOverviewValue.js delete mode 100644 src/ui/scripts/utils/genericSubState.js diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index cebd854f..591f7209 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -5,7 +5,6 @@ export * from './route' export * from './filter' export * from './domains' export * from './events' -export * from './overview' export * from './widgets' export const RESET_STATE = Symbol() diff --git a/src/ui/scripts/actions/overview.js b/src/ui/scripts/actions/overview.js deleted file mode 100644 index 0a9ea5bd..00000000 --- a/src/ui/scripts/actions/overview.js +++ /dev/null @@ -1,158 +0,0 @@ -import api from '../utils/api' -import signalHandler from '../utils/signalHandler' - -import { SORTINGS_TOP } from '../../../constants/sortings' -import { RANGES_LAST_24_HOURS } from '../../../constants/ranges' -import { INTERVALS_DAILY } from '../../../constants/intervals' - -export const ALL_DOMAINS = Symbol() - -export const SET_OVERVIEW_START = Symbol() -export const SET_OVERVIEW_END = Symbol() -export const SET_OVERVIEW_FETCHING = Symbol() -export const SET_OVERVIEW_ERROR = Symbol() - -export const setOverviewStart = (domainId) => ({ - type: SET_OVERVIEW_START, - domainId -}) - -export const setOverviewEnd = (domainId, facts, statistics) => ({ - type: SET_OVERVIEW_END, - domainId, - facts, - statistics -}) - -export const setOverviewFetching = (domainId, payload) => ({ - type: SET_OVERVIEW_FETCHING, - domainId, - payload -}) - -export const setOverviewError = (domainId, payload) => ({ - type: SET_OVERVIEW_ERROR, - domainId, - payload -}) - -export const fetchOverview = signalHandler((signal) => (props, domainId) => async (dispatch) => { - - dispatch(setOverviewStart(domainId)) - - try { - - const facts = ` - facts { - activeVisitors - averageViews - averageDuration - viewsToday - viewsMonth - viewsYear - } - ` - - const statistics = ` - statistics { - views(interval: $interval, type: UNIQUE) { - id - count - } - pages(sorting: $sorting, range: $range) { - id - count - created - } - referrers(sorting: $sorting, range: $range) { - id - count - created - } - durations(interval: $interval) { - id - count - } - systems(sorting: $sorting, type: WITH_VERSION, range: $range) { - id - count - created - } - devices(sorting: $sorting, type: WITH_MODEL, range: $range) { - id - count - created - } - browsers(sorting: $sorting, type: WITH_VERSION, range: $range) { - id - count - created - } - sizes(sorting: $sorting, type: BROWSER_RESOLUTION, range: $range) { - id - count - created - } - languages(sorting: $sorting, range: $range) { - id - count - created - } - } - ` - - if (domainId === ALL_DOMAINS) { - - const data = await api({ - query: ` - query fetchOverview($interval: Interval!, $sorting: Sorting!, $range: Range, ) { - ${ facts } - ${ statistics } - } - `, - variables: { - interval: INTERVALS_DAILY, - sorting: SORTINGS_TOP, - range: RANGES_LAST_24_HOURS - }, - props, - signal: signal(domainId) - }) - - dispatch(setOverviewEnd(domainId, data.facts, data.statistics)) - - } else { - - const data = await api({ - query: ` - query fetchOverview($id: ID!, $interval: Interval!, $sorting: Sorting!, $range: Range) { - domain(id: $id) { - ${ facts } - ${ statistics } - } - } - `, - variables: { - id: domainId, - interval: INTERVALS_DAILY, - sorting: SORTINGS_TOP, - range: RANGES_LAST_24_HOURS - }, - props, - signal: signal(domainId) - }) - - dispatch(setOverviewEnd(domainId, data.domain.facts, data.domain.statistics)) - - } - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setOverviewFetching(domainId, false)) - if (err.name === 'HandledError') return - dispatch(setOverviewError(domainId, err)) - - } - -}) \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteOverview.js b/src/ui/scripts/components/routes/RouteOverview.js index 407ff799..3a5a9b4a 100644 --- a/src/ui/scripts/components/routes/RouteOverview.js +++ b/src/ui/scripts/components/routes/RouteOverview.js @@ -1,4 +1,4 @@ -import { createElement as h, Fragment, useEffect } from 'react' +import { createElement as h, Fragment } from 'react' import { SORTINGS_TOP } from '../../../../constants/sortings' import { RANGES_LAST_24_HOURS } from '../../../../constants/ranges' @@ -10,8 +10,6 @@ import { BROWSERS_TYPE_WITH_VERSION } from '../../../../constants/browsers' import { SIZES_TYPE_BROWSER_RESOLUTION } from '../../../../constants/sizes' import * as route from '../../constants/route' -import { ALL_DOMAINS } from '../../actions/overview' -import * as selectOverviewValue from '../../selectors/selectOverviewValue' import useWidgets from '../../hooks/useWidgets' import mergedViewsLoader from '../../loaders/mergedViewsLoader' @@ -25,19 +23,12 @@ import mergedSizesLoader from '../../loaders/mergedSizesLoader' import mergedLanguagesLoader from '../../loaders/mergedLanguagesLoader' // TODO: Refactor facts -import enhanceFacts from '../../enhancers/enhanceFacts' -import CardFacts from '../cards/CardFacts' +// import enhanceFacts from '../../enhancers/enhanceFacts' +// import CardFacts from '../cards/CardFacts' const RouteOverview = (props) => { - const domainId = props.route.params.domainId || ALL_DOMAINS - const isLoading = selectOverviewValue.withoutType(props, domainId).fetching - - useEffect(() => { - - props.fetchOverview(props, domainId) - - }, [ domainId ]) + // const domainId = props.route.params.domainId || ALL_DOMAINS const renderedEssentialWidgets = useWidgets(props, [ { @@ -142,10 +133,9 @@ const RouteOverview = (props) => { return ( h(Fragment, {}, - h(CardFacts, { - loading: isLoading, - items: enhanceFacts(selectOverviewValue.withoutType(props, domainId).facts) - }), + // h(CardFacts, { + // items: enhanceFacts(selectOverviewValue.withoutType(props, domainId).facts) + // }), h('div', { className: 'content__spacer' }), renderedEssentialWidgets, h('div', { className: 'content__spacer' }), diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index ca484bbb..ba03c9f5 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -1,12 +1,8 @@ -import { ALL_DOMAINS } from '../actions/overview' import isDefined from '../../../utils/isDefined' -import * as selectOverviewValue from '../selectors/selectOverviewValue' export default (state) => { const fetching = ( - selectOverviewValue.withoutType(state, ALL_DOMAINS).fetching === true || - Object.values(state.overview.value).some((value) => value.fetching) === true || Object.values(state.widgets.value).some((value) => value.fetching) === true || state.domains.fetching === true || state.token.fetching === true || @@ -15,8 +11,6 @@ export default (state) => { ) const errors = [ - selectOverviewValue.withoutType(state, ALL_DOMAINS).error, - ...Object.values(state.overview.value).map((value) => value.error), ...Object.values(state.widgets.value).map((value) => value.error), state.domains.error, state.token.error, diff --git a/src/ui/scripts/reducers/index.js b/src/ui/scripts/reducers/index.js index 85fdc79a..eb901d8f 100644 --- a/src/ui/scripts/reducers/index.js +++ b/src/ui/scripts/reducers/index.js @@ -11,7 +11,6 @@ import route from './route' import filter from './filter' import domains from './domains' import events from './events' -import overview from './overview' import widgets from './widgets' const reducers = combineReducers({ @@ -22,7 +21,6 @@ const reducers = combineReducers({ filter, domains, events, - overview, widgets }) diff --git a/src/ui/scripts/reducers/overview.js b/src/ui/scripts/reducers/overview.js deleted file mode 100644 index 8b13ef5d..00000000 --- a/src/ui/scripts/reducers/overview.js +++ /dev/null @@ -1,46 +0,0 @@ -import produce from 'immer' - -import { - SET_OVERVIEW_START, - SET_OVERVIEW_END, - SET_OVERVIEW_FETCHING, - SET_OVERVIEW_ERROR -} from '../actions' - -export const initialState = () => ({ - value: {} -}) - -export const initialSubState = () => ({ - facts: {}, - statistics: {}, - fetching: false, - error: undefined -}) - -export default produce((draft, action) => { - - const hasDomainId = () => action.domainId != null - const hasDomainValue = () => draft.value[action.domainId] != null - - if (hasDomainId() === true && hasDomainValue() === false) draft.value[action.domainId] = initialSubState() - - switch (action.type) { - case SET_OVERVIEW_START: - draft.value[action.domainId].fetching = true - draft.value[action.domainId].error = action.payload || initialSubState().error - break - case SET_OVERVIEW_END: - draft.value[action.domainId].facts = action.facts || initialSubState().facts - draft.value[action.domainId].statistics = action.statistics || initialSubState().statistics - draft.value[action.domainId].fetching = false - break - case SET_OVERVIEW_FETCHING: - draft.value[action.domainId].fetching = action.payload || initialSubState().fetching - break - case SET_OVERVIEW_ERROR: - draft.value[action.domainId].error = action.payload || initialSubState().error - break - } - -}, initialState()) \ No newline at end of file diff --git a/src/ui/scripts/selectors/selectOverviewValue.js b/src/ui/scripts/selectors/selectOverviewValue.js deleted file mode 100644 index 9415eae5..00000000 --- a/src/ui/scripts/selectors/selectOverviewValue.js +++ /dev/null @@ -1,12 +0,0 @@ -import { initialSubState } from '../reducers/overview' -import genericSubState from '../utils/genericSubState' - -export const withoutType = (state, domainId) => { - const value = state.overview.value[domainId] - return value == null ? initialSubState() : value -} - -export const withType = (state, domainId, type) => { - const value = withoutType(state, domainId).statistics[type] - return value == null ? genericSubState().value : value -} \ No newline at end of file diff --git a/src/ui/scripts/utils/genericSubState.js b/src/ui/scripts/utils/genericSubState.js deleted file mode 100644 index 6e77c648..00000000 --- a/src/ui/scripts/utils/genericSubState.js +++ /dev/null @@ -1,3 +0,0 @@ -export default () => ({ - value: [] -}) \ No newline at end of file From 66709d8c1d0c6403568c828a18589093a61e4d8d Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 16:48:06 +0100 Subject: [PATCH 076/208] Use initial state for params --- src/ui/scripts/actions/route.js | 2 +- src/ui/scripts/reducers/route.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/actions/route.js b/src/ui/scripts/actions/route.js index 619e709a..59cda386 100644 --- a/src/ui/scripts/actions/route.js +++ b/src/ui/scripts/actions/route.js @@ -3,5 +3,5 @@ export const SET_ROUTE = Symbol() export const setRoute = (payload) => ({ type: SET_ROUTE, key: payload.key, - params: payload.params || {} + params: payload.params }) \ No newline at end of file diff --git a/src/ui/scripts/reducers/route.js b/src/ui/scripts/reducers/route.js index b318b42c..b71a208c 100644 --- a/src/ui/scripts/reducers/route.js +++ b/src/ui/scripts/reducers/route.js @@ -16,7 +16,7 @@ export default produce((draft, action) => { switch (action.type) { case SET_ROUTE: draft.key = action.key - draft.params = action.params + draft.params = action.params || initialState().params break } From b52e357a08260f171d33d83dde9cf0287afbbe3a Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 16:48:50 +0100 Subject: [PATCH 077/208] Custom route for domains, facts loader, useWidgets for all widgets --- src/ui/scripts/components/Dashboard.js | 8 +- .../scripts/components/routes/RouteDomain.js | 151 ++++++++++++++++++ .../components/routes/RouteOverview.js | 15 +- src/ui/scripts/constants/route.js | 2 + src/ui/scripts/hooks/useDomainWidgets.js | 49 ++---- src/ui/scripts/hooks/useMergedWidget.js | 26 +-- src/ui/scripts/hooks/useWidgets.js | 11 +- src/ui/scripts/loaders/factsLoader.js | 39 +++++ src/ui/scripts/loaders/languagesLoader.js | 2 +- src/ui/scripts/loaders/mergedFactsLoader.js | 35 ++++ .../{overviewRoute.js => domainRoute.js} | 4 +- 11 files changed, 269 insertions(+), 73 deletions(-) create mode 100644 src/ui/scripts/components/routes/RouteDomain.js create mode 100644 src/ui/scripts/loaders/factsLoader.js create mode 100644 src/ui/scripts/loaders/mergedFactsLoader.js rename src/ui/scripts/utils/{overviewRoute.js => domainRoute.js} (53%) diff --git a/src/ui/scripts/components/Dashboard.js b/src/ui/scripts/components/Dashboard.js index 756a749c..43fc8890 100644 --- a/src/ui/scripts/components/Dashboard.js +++ b/src/ui/scripts/components/Dashboard.js @@ -5,7 +5,7 @@ import * as route from '../constants/route' import * as selectDomainsValue from '../selectors/selectDomainsValue' import routeByKey from '../utils/routeByKey' import whenBelow from '../utils/whenBelow' -import overviewRoute from '../utils/overviewRoute' +import domainRoute from '../utils/domainRoute' import isDefined from '../../../utils/isDefined' import Header, { createButton, createDropdown, createDropdownButton, createDropdownSeparator } from './Header' @@ -14,7 +14,7 @@ import Modals from './Modals' const gotoDomainWhenDefined = (props, index) => { const domain = selectDomainsValue.byIndex(props, index) - if (domain != null) props.setRoute(overviewRoute(domain)) + if (domain != null) props.setRoute(domainRoute(domain)) } @@ -56,11 +56,11 @@ const Dashboard = (props) => { const hasDomains = props.domains.value.length > 0 - const domainsLabel = (activeInside) => activeInside === true ? selectDomainsValue.byId(props, props.route.params.domainId).title : 'Domains' + const domainsLabel = (activeInside) => activeInside === true ? selectDomainsValue.byId(props, props.route.params.domainId).title : route.ROUTE_DOMAIN.title const insightsLabel = (activeInside) => activeInside === true ? routeByKey(props.route.key).title : 'Insights' const domainsItems = props.domains.value.map((domain, index) => - createDropdownButton(domain.title, overviewRoute(domain), props, whenBelow(index, 10)) + createDropdownButton(domain.title, domainRoute(domain), props, whenBelow(index, 10)) ) const insightsItems = [ diff --git a/src/ui/scripts/components/routes/RouteDomain.js b/src/ui/scripts/components/routes/RouteDomain.js new file mode 100644 index 00000000..770b1aa2 --- /dev/null +++ b/src/ui/scripts/components/routes/RouteDomain.js @@ -0,0 +1,151 @@ +import { createElement as h, Fragment, useMemo } from 'react' + +import { SORTINGS_TOP } from '../../../../constants/sortings' +import { RANGES_LAST_24_HOURS } from '../../../../constants/ranges' +import { INTERVALS_DAILY } from '../../../../constants/intervals' +import { VIEWS_TYPE_UNIQUE } from '../../../../constants/views' +import { SYSTEMS_TYPE_WITH_VERSION } from '../../../../constants/systems' +import { DEVICES_TYPE_WITH_MODEL } from '../../../../constants/devices' +import { BROWSERS_TYPE_WITH_VERSION } from '../../../../constants/browsers' +import { SIZES_TYPE_BROWSER_RESOLUTION } from '../../../../constants/sizes' + +import * as route from '../../constants/route' +import useWidgets from '../../hooks/useWidgets' + +import viewsLoader from '../../loaders/viewsLoader' +import durationsLoader from '../../loaders/durationsLoader' +import pagesLoader from '../../loaders/pagesLoader' +import referrersLoader from '../../loaders/referrersLoader' +import systemsLoader from '../../loaders/systemsLoader' +import devicesLoader from '../../loaders/devicesLoader' +import browsersLoader from '../../loaders/browsersLoader' +import sizesLoader from '../../loaders/sizesLoader' +import languagesLoader from '../../loaders/languagesLoader' + +// TODO: Refactor facts +// import enhanceFacts from '../../enhancers/enhanceFacts' +// import CardFacts from '../cards/CardFacts' + +const RouteDomain = (props) => { + + const domainId = props.route.params.domainId + + const essentialWidgetConfigs = useMemo(() => [ + { + loader: viewsLoader(domainId, { + interval: INTERVALS_DAILY, + type: VIEWS_TYPE_UNIQUE + }), + additionalProps: { + wide: true, + headline: 'Site Views', + onMore: () => props.setRoute(route.ROUTE_VIEWS) + } + }, + { + loader: durationsLoader(domainId, { + interval: INTERVALS_DAILY + }), + additionalProps: { + wide: true, + headline: 'Durations', + onMore: () => props.setRoute(route.ROUTE_DURATIONS) + } + }, + { + loader: pagesLoader(domainId, { + range: RANGES_LAST_24_HOURS, + sorting: SORTINGS_TOP + }), + additionalProps: { + headline: 'Pages', + onMore: () => props.setRoute(route.ROUTE_PAGES) + } + }, + { + loader: referrersLoader(domainId, { + range: RANGES_LAST_24_HOURS, + sorting: SORTINGS_TOP + }), + additionalProps: { + headline: 'Referrers', + onMore: () => props.setRoute(route.ROUTE_REFERRERS) + } + } + ], []) + + const detailedWidgetConfigs = useMemo(() => [ + { + loader: systemsLoader(domainId, { + sorting: SORTINGS_TOP, + range: RANGES_LAST_24_HOURS, + type: SYSTEMS_TYPE_WITH_VERSION + }), + additionalProps: { + headline: 'Systems', + onMore: () => props.setRoute(route.ROUTE_SYSTEMS) + } + }, + { + loader: devicesLoader(domainId, { + sorting: SORTINGS_TOP, + range: RANGES_LAST_24_HOURS, + type: DEVICES_TYPE_WITH_MODEL + }), + additionalProps: { + headline: 'Devices', + onMore: () => props.setRoute(route.ROUTE_DEVICES) + } + }, + { + loader: browsersLoader(domainId, { + sorting: SORTINGS_TOP, + range: RANGES_LAST_24_HOURS, + type: BROWSERS_TYPE_WITH_VERSION + }), + additionalProps: { + headline: 'Browsers', + onMore: () => props.setRoute(route.ROUTE_BROWSERS) + } + }, + { + loader: sizesLoader(domainId, { + sorting: SORTINGS_TOP, + range: RANGES_LAST_24_HOURS, + type: SIZES_TYPE_BROWSER_RESOLUTION + }), + additionalProps: { + headline: 'Sizes', + onMore: () => props.setRoute(route.ROUTE_SIZES) + } + }, + { + loader: languagesLoader(domainId, { + sorting: SORTINGS_TOP, + range: RANGES_LAST_24_HOURS + }), + additionalProps: { + headline: 'Languages', + onMore: () => props.setRoute(route.ROUTE_LANGUAGES) + } + } + ], []) + + const renderedEssentialWidgets = useWidgets(props, essentialWidgetConfigs) + const renderedDetailedWidgets = useWidgets(props, detailedWidgetConfigs) + + return ( + h(Fragment, {}, + // h(CardFacts, { + // items: enhanceFacts(selectOverviewValue.withoutType(props, domainId).facts) + // }), + h('div', { className: 'content__spacer' }), + renderedEssentialWidgets, + h('div', { className: 'content__spacer' }), + renderedDetailedWidgets + ) + ) + +} + +export default RouteDomain \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteOverview.js b/src/ui/scripts/components/routes/RouteOverview.js index 3a5a9b4a..f366025a 100644 --- a/src/ui/scripts/components/routes/RouteOverview.js +++ b/src/ui/scripts/components/routes/RouteOverview.js @@ -1,4 +1,4 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h, Fragment, useMemo } from 'react' import { SORTINGS_TOP } from '../../../../constants/sortings' import { RANGES_LAST_24_HOURS } from '../../../../constants/ranges' @@ -28,9 +28,7 @@ import mergedLanguagesLoader from '../../loaders/mergedLanguagesLoader' const RouteOverview = (props) => { - // const domainId = props.route.params.domainId || ALL_DOMAINS - - const renderedEssentialWidgets = useWidgets(props, [ + const essentialWidgetConfigs = useMemo(() => [ { loader: mergedViewsLoader({ interval: INTERVALS_DAILY, @@ -72,9 +70,9 @@ const RouteOverview = (props) => { onMore: () => props.setRoute(route.ROUTE_REFERRERS) } } - ]) + ], []) - const renderedDetailedWidgets = useWidgets(props, [ + const detailedWidgetConfigs = useMemo(() => [ { loader: mergedSystemsLoader({ sorting: SORTINGS_TOP, @@ -129,7 +127,10 @@ const RouteOverview = (props) => { onMore: () => props.setRoute(route.ROUTE_LANGUAGES) } } - ]) + ], []) + + const renderedEssentialWidgets = useWidgets(props, essentialWidgetConfigs) + const renderedDetailedWidgets = useWidgets(props, detailedWidgetConfigs) return ( h(Fragment, {}, diff --git a/src/ui/scripts/constants/route.js b/src/ui/scripts/constants/route.js index 00762045..11231ca2 100644 --- a/src/ui/scripts/constants/route.js +++ b/src/ui/scripts/constants/route.js @@ -1,4 +1,5 @@ import RouteOverview from '../components/routes/RouteOverview' +import RouteDomain from '../components/routes/RouteDomain' import RouteViews from '../components/routes/RouteViews' import RoutePages from '../components/routes/RoutePages' import RouteReferrers from '../components/routes/RouteReferrers' @@ -12,6 +13,7 @@ import RouteEvents from '../components/routes/RouteEvents' import RouteSettings from '../components/routes/RouteSettings' export const ROUTE_OVERVIEW = { key: 'route_overview', title: 'Overview', component: RouteOverview } +export const ROUTE_DOMAIN = { key: 'route_domain', title: 'Domains', component: RouteDomain } export const ROUTE_VIEWS = { key: 'route_views', title: 'Views', component: RouteViews } export const ROUTE_PAGES = { key: 'route_pages', title: 'Pages', component: RoutePages } export const ROUTE_REFERRERS = { key: 'route_referrers', title: 'Referrers', component: RouteReferrers } diff --git a/src/ui/scripts/hooks/useDomainWidgets.js b/src/ui/scripts/hooks/useDomainWidgets.js index e0c78633..9a4cb5bd 100644 --- a/src/ui/scripts/hooks/useDomainWidgets.js +++ b/src/ui/scripts/hooks/useDomainWidgets.js @@ -1,45 +1,22 @@ -import { createElement as h, useEffect, useState } from 'react' +import { createElement as h, useMemo } from 'react' -import CardWidget from '../components/cards/CardWidget' +import useWidgets from './useWidgets' +import domainRoute from '../utils/domainRoute' -import { initialSubState } from '../reducers/widgets' -import * as selectDomainsValue from '../selectors/selectDomainsValue' -import overviewRoute from '../utils/overviewRoute' +export default (props, createLoader, opts) => { -export default (props, createLoader, opts, additionalProps = {}) => { + const widgetConfigs = useMemo(() => { - const [ widgetIds, setWidgetIds ] = useState([]) - - useEffect(() => { - - const loaders = props.domains.value.map((domain) => - createLoader(domain.id, opts) - ) - - const widgetIds = loaders.map((loader) => - loader.id - ) - - if (loaders.length > 0) { - props.fetchWidgets(props, loaders) - setWidgetIds(widgetIds) - } + return props.domains.value.map((domain) => ({ + loader: createLoader(domain.id, opts), + additionalProps: { + headline: domain.title, + onMore: () => props.setRoute(domainRoute(domain)) + } + })) }, [ props.domains.value, ...Object.values(opts) ]) - return widgetIds.map( - (widgetId) => { - const widgetData = props.widgets.value[widgetId] || initialSubState() - const domain = selectDomainsValue.byId(props, widgetData.variables.domainId) - - return h(CardWidget, { - key: domain.id, - headline: domain.title, - widget: widgetData, - onMore: () => props.setRoute(overviewRoute(domain)), - ...additionalProps - }) - } - ) + return useWidgets(props, widgetConfigs) } \ No newline at end of file diff --git a/src/ui/scripts/hooks/useMergedWidget.js b/src/ui/scripts/hooks/useMergedWidget.js index d9e8515e..56db3d6c 100644 --- a/src/ui/scripts/hooks/useMergedWidget.js +++ b/src/ui/scripts/hooks/useMergedWidget.js @@ -1,28 +1,18 @@ -import { createElement as h, useEffect, useState } from 'react' +import { createElement as h, useMemo } from 'react' -import CardWidget from '../components/cards/CardWidget' - -import { initialSubState } from '../reducers/widgets' +import useWidgets from './useWidgets' export default (props, createLoader, opts, additionalProps = {}) => { - const [ widgetId, setWidgetId ] = useState() - - useEffect(() => { + const widgetConfigs = useMemo(() => { - const loader = createLoader(opts) - props.fetchWidget(props, loader) - - setWidgetId(loader.id) + return [{ + loader: createLoader(opts), + additionalProps + }] }, [ ...Object.values(opts) ]) - const widgetData = props.widgets.value[widgetId] || initialSubState() - - return h(CardWidget, { - key: widgetId, - widget: widgetData, - ...additionalProps - }) + return useWidgets(props, widgetConfigs) } \ No newline at end of file diff --git a/src/ui/scripts/hooks/useWidgets.js b/src/ui/scripts/hooks/useWidgets.js index 10e08342..74bf770b 100644 --- a/src/ui/scripts/hooks/useWidgets.js +++ b/src/ui/scripts/hooks/useWidgets.js @@ -1,4 +1,4 @@ -import { createElement as h, useMemo } from 'react' +import { createElement as h, useState, useEffect } from 'react' import { initialSubState } from '../reducers/widgets' @@ -6,7 +6,9 @@ import CardWidget from '../components/cards/CardWidget' export default (props, widgetConfigs = []) => { - const widgetIds = useMemo(() => { + const [ widgetIds, setWidgetIds ] = useState([]) + + useEffect(() => { const loaders = widgetConfigs.map((widgetConfig) => widgetConfig.loader @@ -17,10 +19,9 @@ export default (props, widgetConfigs = []) => { ) props.fetchWidgets(props, loaders) - return widgetIds + setWidgetIds(widgetIds) - // TODO: Rerender when widgetConfigs changes - }, []) + }, [ widgetConfigs ]) return widgetIds.map( (widgetId, index) => { diff --git a/src/ui/scripts/loaders/factsLoader.js b/src/ui/scripts/loaders/factsLoader.js new file mode 100644 index 00000000..bd873df2 --- /dev/null +++ b/src/ui/scripts/loaders/factsLoader.js @@ -0,0 +1,39 @@ +import { createElement as h } from 'react' + +import RendererList from '../components/renderers/RendererList' +import enhanceFacts from '../enhancers/enhanceFacts' +import createWidgetId from '../utils/createWidgetId' + +export default (domainId, opts) => { + + const id = createWidgetId('fetchLanguages', domainId, opts) + + const query = ` + domain(id: "${ domainId }") { + facts { + activeVisitors + averageViews + averageDuration + viewsToday + viewsMonth + viewsYear + } + } + ` + + const variables = { + domainId + } + + const selector = (data, entryName = 'domain') => data[entryName].facts + + return { + id, + Renderer: RendererList, + query, + variables, + selector, + enhancer: enhanceFacts + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/languagesLoader.js b/src/ui/scripts/loaders/languagesLoader.js index 0a7b4c7c..206c176e 100644 --- a/src/ui/scripts/loaders/languagesLoader.js +++ b/src/ui/scripts/loaders/languagesLoader.js @@ -6,7 +6,7 @@ import createWidgetId from '../utils/createWidgetId' export default (domainId, opts) => { - const id = createWidgetId('fetchLanguages', domainId, opts) + const id = createWidgetId('fetchFacts', domainId, opts) const query = ` domain(id: "${ domainId }") { diff --git a/src/ui/scripts/loaders/mergedFactsLoader.js b/src/ui/scripts/loaders/mergedFactsLoader.js new file mode 100644 index 00000000..8a4c3c7e --- /dev/null +++ b/src/ui/scripts/loaders/mergedFactsLoader.js @@ -0,0 +1,35 @@ +import { createElement as h } from 'react' + +import RendererList from '../components/renderers/RendererList' +import enhanceFacts from '../enhancers/enhanceFacts' +import createWidgetId from '../utils/createWidgetId' + +export default (opts) => { + + const id = createWidgetId('fetchFacts', undefined, opts) + + const query = ` + facts { + activeVisitors + averageViews + averageDuration + viewsToday + viewsMonth + viewsYear + } + ` + + const variables = {} + + const selector = (data, entryName = 'facts') => data[entryName] + + return { + id, + Renderer: RendererList, + query, + variables, + selector, + enhancer: enhanceFacts + } + +} \ No newline at end of file diff --git a/src/ui/scripts/utils/overviewRoute.js b/src/ui/scripts/utils/domainRoute.js similarity index 53% rename from src/ui/scripts/utils/overviewRoute.js rename to src/ui/scripts/utils/domainRoute.js index 6f000db1..f70b5b74 100644 --- a/src/ui/scripts/utils/overviewRoute.js +++ b/src/ui/scripts/utils/domainRoute.js @@ -1,9 +1,9 @@ -import { ROUTE_OVERVIEW } from '../constants/route' +import { ROUTE_DOMAIN } from '../constants/route' export default (domain) => { return { - ...ROUTE_OVERVIEW, + ...ROUTE_DOMAIN, params: { domainId: domain.id } From 40c4c02ea7bd09dc691316b7c634b74ea7a701c5 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 17:17:12 +0100 Subject: [PATCH 078/208] Rename hook --- src/ui/scripts/components/routes/RouteDomain.js | 6 +++--- src/ui/scripts/components/routes/RouteOverview.js | 6 +++--- src/ui/scripts/hooks/{useWidgets.js => useCardWidgets.js} | 0 src/ui/scripts/hooks/useDomainWidgets.js | 4 ++-- src/ui/scripts/hooks/useMergedWidget.js | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename src/ui/scripts/hooks/{useWidgets.js => useCardWidgets.js} (100%) diff --git a/src/ui/scripts/components/routes/RouteDomain.js b/src/ui/scripts/components/routes/RouteDomain.js index 770b1aa2..08de7d2f 100644 --- a/src/ui/scripts/components/routes/RouteDomain.js +++ b/src/ui/scripts/components/routes/RouteDomain.js @@ -10,7 +10,7 @@ import { BROWSERS_TYPE_WITH_VERSION } from '../../../../constants/browsers' import { SIZES_TYPE_BROWSER_RESOLUTION } from '../../../../constants/sizes' import * as route from '../../constants/route' -import useWidgets from '../../hooks/useWidgets' +import useCardWidgets from '../../hooks/useCardWidgets' import viewsLoader from '../../loaders/viewsLoader' import durationsLoader from '../../loaders/durationsLoader' @@ -131,8 +131,8 @@ const RouteDomain = (props) => { } ], []) - const renderedEssentialWidgets = useWidgets(props, essentialWidgetConfigs) - const renderedDetailedWidgets = useWidgets(props, detailedWidgetConfigs) + const renderedEssentialWidgets = useCardWidgets(props, essentialWidgetConfigs) + const renderedDetailedWidgets = useCardWidgets(props, detailedWidgetConfigs) return ( h(Fragment, {}, diff --git a/src/ui/scripts/components/routes/RouteOverview.js b/src/ui/scripts/components/routes/RouteOverview.js index f366025a..23ea633c 100644 --- a/src/ui/scripts/components/routes/RouteOverview.js +++ b/src/ui/scripts/components/routes/RouteOverview.js @@ -10,7 +10,7 @@ import { BROWSERS_TYPE_WITH_VERSION } from '../../../../constants/browsers' import { SIZES_TYPE_BROWSER_RESOLUTION } from '../../../../constants/sizes' import * as route from '../../constants/route' -import useWidgets from '../../hooks/useWidgets' +import useCardWidgets from '../../hooks/useCardWidgets' import mergedViewsLoader from '../../loaders/mergedViewsLoader' import mergedDurationsLoader from '../../loaders/mergedDurationsLoader' @@ -129,8 +129,8 @@ const RouteOverview = (props) => { } ], []) - const renderedEssentialWidgets = useWidgets(props, essentialWidgetConfigs) - const renderedDetailedWidgets = useWidgets(props, detailedWidgetConfigs) + const renderedEssentialWidgets = useCardWidgets(props, essentialWidgetConfigs) + const renderedDetailedWidgets = useCardWidgets(props, detailedWidgetConfigs) return ( h(Fragment, {}, diff --git a/src/ui/scripts/hooks/useWidgets.js b/src/ui/scripts/hooks/useCardWidgets.js similarity index 100% rename from src/ui/scripts/hooks/useWidgets.js rename to src/ui/scripts/hooks/useCardWidgets.js diff --git a/src/ui/scripts/hooks/useDomainWidgets.js b/src/ui/scripts/hooks/useDomainWidgets.js index 9a4cb5bd..ee71f649 100644 --- a/src/ui/scripts/hooks/useDomainWidgets.js +++ b/src/ui/scripts/hooks/useDomainWidgets.js @@ -1,6 +1,6 @@ import { createElement as h, useMemo } from 'react' -import useWidgets from './useWidgets' +import useCardWidgets from './useCardWidgets' import domainRoute from '../utils/domainRoute' export default (props, createLoader, opts) => { @@ -17,6 +17,6 @@ export default (props, createLoader, opts) => { }, [ props.domains.value, ...Object.values(opts) ]) - return useWidgets(props, widgetConfigs) + return useCardWidgets(props, widgetConfigs) } \ No newline at end of file diff --git a/src/ui/scripts/hooks/useMergedWidget.js b/src/ui/scripts/hooks/useMergedWidget.js index 56db3d6c..2cb98c6c 100644 --- a/src/ui/scripts/hooks/useMergedWidget.js +++ b/src/ui/scripts/hooks/useMergedWidget.js @@ -1,6 +1,6 @@ import { createElement as h, useMemo } from 'react' -import useWidgets from './useWidgets' +import useCardWidgets from './useCardWidgets' export default (props, createLoader, opts, additionalProps = {}) => { @@ -13,6 +13,6 @@ export default (props, createLoader, opts, additionalProps = {}) => { }, [ ...Object.values(opts) ]) - return useWidgets(props, widgetConfigs) + return useCardWidgets(props, widgetConfigs) } \ No newline at end of file From c3e0ade265cf1b2d78ea1f5a3a08b3518af3ce66 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 17:26:38 +0100 Subject: [PATCH 079/208] Rename hook and remove useMergedWidget --- .../components/routes/RouteBrowsers.js | 4 +- .../scripts/components/routes/RouteDevices.js | 4 +- .../components/routes/RouteDurations.js | 33 +++++++++------ .../components/routes/RouteLanguages.js | 4 +- .../scripts/components/routes/RoutePages.js | 4 +- .../components/routes/RouteReferrers.js | 4 +- .../scripts/components/routes/RouteSizes.js | 4 +- .../scripts/components/routes/RouteSystems.js | 4 +- .../scripts/components/routes/RouteViews.js | 40 +++++++++++-------- ...Widgets.js => useCardWidgetsForDomains.js} | 0 src/ui/scripts/hooks/useMergedWidget.js | 18 --------- 11 files changed, 57 insertions(+), 62 deletions(-) rename src/ui/scripts/hooks/{useDomainWidgets.js => useCardWidgetsForDomains.js} (100%) delete mode 100644 src/ui/scripts/hooks/useMergedWidget.js diff --git a/src/ui/scripts/components/routes/RouteBrowsers.js b/src/ui/scripts/components/routes/RouteBrowsers.js index 89f38e2c..9b272526 100644 --- a/src/ui/scripts/components/routes/RouteBrowsers.js +++ b/src/ui/scripts/components/routes/RouteBrowsers.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import browsersLoader from '../../loaders/browsersLoader' -import useDomainWidgets from '../../hooks/useDomainWidgets' +import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' const RouteBrowsers = (props) => { - return useDomainWidgets(props, browsersLoader, { + return useCardWidgetsForDomains(props, browsersLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.browsersType diff --git a/src/ui/scripts/components/routes/RouteDevices.js b/src/ui/scripts/components/routes/RouteDevices.js index 68544b80..590efcd9 100644 --- a/src/ui/scripts/components/routes/RouteDevices.js +++ b/src/ui/scripts/components/routes/RouteDevices.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import devicesLoader from '../../loaders/devicesLoader' -import useDomainWidgets from '../../hooks/useDomainWidgets' +import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' const RouteDevices = (props) => { - return useDomainWidgets(props, devicesLoader, { + return useCardWidgetsForDomains(props, devicesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.devicesType diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index 39791d2f..de454f5b 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -1,27 +1,34 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h, Fragment, useMemo } from 'react' import mergedDurationsLoader from '../../loaders/mergedDurationsLoader' import durationsLoader from '../../loaders/durationsLoader' -import useMergedWidget from '../../hooks/useMergedWidget' -import useDomainWidgets from '../../hooks/useDomainWidgets' +import useCardWidgets from '../../hooks/useCardWidgets' +import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' const RouteDurations = (props) => { - const renderedMergedWidget = useMergedWidget(props, mergedDurationsLoader, { - interval: props.filter.interval - }, { - wide: true, - headline: () => 'Durations' - }) + const mergedWidgetConfigs = useMemo(() => { + + return [{ + loader: mergedDurationsLoader({ + interval: props.filter.interval + }), + additionalProps: { + wide: true, + headline: 'Durations' + } + }] - const renderedDomainWidgets = useDomainWidgets(props, durationsLoader, { - interval: props.filter.interval, - type: props.filter.viewsType + }, [ props.filter.interval ]) + + const renderedMergedWidgets = useCardWidgets(props, mergedWidgetConfigs) + const renderedDomainWidgets = useCardWidgetsForDomains(props, durationsLoader, { + interval: props.filter.interval }) return ( h(Fragment, {}, - renderedMergedWidget, + renderedMergedWidgets, renderedDomainWidgets ) ) diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index bb794c9c..b14b249c 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import languagesLoader from '../../loaders/languagesLoader' -import useDomainWidgets from '../../hooks/useDomainWidgets' +import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' const RouteLanguages = (props) => { - return useDomainWidgets(props, languagesLoader, { + return useCardWidgetsForDomains(props, languagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index 79f2efcc..1d17feb5 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import pagesLoader from '../../loaders/pagesLoader' -import useDomainWidgets from '../../hooks/useDomainWidgets' +import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' const RoutePages = (props) => { - return useDomainWidgets(props, pagesLoader, { + return useCardWidgetsForDomains(props, pagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) diff --git a/src/ui/scripts/components/routes/RouteReferrers.js b/src/ui/scripts/components/routes/RouteReferrers.js index b821484f..988bc8a0 100644 --- a/src/ui/scripts/components/routes/RouteReferrers.js +++ b/src/ui/scripts/components/routes/RouteReferrers.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import referrersLoader from '../../loaders/referrersLoader' -import useDomainWidgets from '../../hooks/useDomainWidgets' +import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' const RouteReferrers = (props) => { - return useDomainWidgets(props, referrersLoader, { + return useCardWidgetsForDomains(props, referrersLoader, { range: props.filter.range, sorting: props.filter.sorting }) diff --git a/src/ui/scripts/components/routes/RouteSizes.js b/src/ui/scripts/components/routes/RouteSizes.js index 34a5d84f..e10302f2 100644 --- a/src/ui/scripts/components/routes/RouteSizes.js +++ b/src/ui/scripts/components/routes/RouteSizes.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import sizesLoader from '../../loaders/sizesLoader' -import useDomainWidgets from '../../hooks/useDomainWidgets' +import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' const RouteSizes = (props) => { - return useDomainWidgets(props, sizesLoader, { + return useCardWidgetsForDomains(props, sizesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.sizesType diff --git a/src/ui/scripts/components/routes/RouteSystems.js b/src/ui/scripts/components/routes/RouteSystems.js index b7ca3855..dddf655c 100644 --- a/src/ui/scripts/components/routes/RouteSystems.js +++ b/src/ui/scripts/components/routes/RouteSystems.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import systemsLoader from '../../loaders/systemsLoader' -import useDomainWidgets from '../../hooks/useDomainWidgets' +import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' const RouteSystems = (props) => { - return useDomainWidgets(props, systemsLoader, { + return useCardWidgetsForDomains(props, systemsLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.systemsType diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index c4b5d069..10ed5442 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -1,34 +1,40 @@ -import { createElement as h, Fragment } from 'react' +import { createElement as h, Fragment, useMemo } from 'react' import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' import mergedViewsLoader from '../../loaders/mergedViewsLoader' import viewsLoader from '../../loaders/viewsLoader' -import useMergedWidget from '../../hooks/useMergedWidget' -import useDomainWidgets from '../../hooks/useDomainWidgets' +import useCardWidgets from '../../hooks/useCardWidgets' +import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' const RouteViews = (props) => { - const renderedMergedWidget = useMergedWidget(props, mergedViewsLoader, { - interval: props.filter.interval, - type: props.filter.viewsType - }, { - wide: true, - headline: (widget) => { - return ({ - [VIEWS_TYPE_UNIQUE]: 'Site Views', - [VIEWS_TYPE_TOTAL]: 'Page Views' - })[widget.variables.type] - } - }) + const mergedWidgetConfigs = useMemo(() => { + + return [{ + loader: mergedViewsLoader({ + interval: props.filter.interval, + type: props.filter.viewsType + }), + additionalProps: { + wide: true, + headline: ({ + [VIEWS_TYPE_UNIQUE]: 'Site Views', + [VIEWS_TYPE_TOTAL]: 'Page Views' + })[props.filter.viewsType] + } + }] + + }, [ props.filter.interval, props.filter.viewsType ]) - const renderedDomainWidgets = useDomainWidgets(props, viewsLoader, { + const renderedMergedWidgets = useCardWidgets(props, mergedWidgetConfigs) + const renderedDomainWidgets = useCardWidgetsForDomains(props, viewsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) return ( h(Fragment, {}, - renderedMergedWidget, + renderedMergedWidgets, renderedDomainWidgets ) ) diff --git a/src/ui/scripts/hooks/useDomainWidgets.js b/src/ui/scripts/hooks/useCardWidgetsForDomains.js similarity index 100% rename from src/ui/scripts/hooks/useDomainWidgets.js rename to src/ui/scripts/hooks/useCardWidgetsForDomains.js diff --git a/src/ui/scripts/hooks/useMergedWidget.js b/src/ui/scripts/hooks/useMergedWidget.js deleted file mode 100644 index 2cb98c6c..00000000 --- a/src/ui/scripts/hooks/useMergedWidget.js +++ /dev/null @@ -1,18 +0,0 @@ -import { createElement as h, useMemo } from 'react' - -import useCardWidgets from './useCardWidgets' - -export default (props, createLoader, opts, additionalProps = {}) => { - - const widgetConfigs = useMemo(() => { - - return [{ - loader: createLoader(opts), - additionalProps - }] - - }, [ ...Object.values(opts) ]) - - return useCardWidgets(props, widgetConfigs) - -} \ No newline at end of file From 797af4894c1ab2bb0fa1358dcd15cd3370f05a21 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 19:44:32 +0100 Subject: [PATCH 080/208] Facts, default value --- src/ui/scripts/actions/widgets.js | 40 +++---------------- .../{CardFacts.js => CardFactsWidget.js} | 40 ++++++++++--------- .../components/routes/RouteBrowsers.js | 4 +- .../scripts/components/routes/RouteDevices.js | 4 +- .../scripts/components/routes/RouteDomain.js | 27 ++++++++----- .../components/routes/RouteDurations.js | 8 ++-- .../components/routes/RouteLanguages.js | 4 +- .../components/routes/RouteOverview.js | 23 ++++++----- .../scripts/components/routes/RoutePages.js | 4 +- .../components/routes/RouteReferrers.js | 4 +- .../scripts/components/routes/RouteSizes.js | 4 +- .../scripts/components/routes/RouteSystems.js | 4 +- .../scripts/components/routes/RouteViews.js | 8 ++-- src/ui/scripts/enhancers/enhanceBrowsers.js | 2 +- src/ui/scripts/enhancers/enhanceDevices.js | 2 +- src/ui/scripts/enhancers/enhanceDurations.js | 2 +- src/ui/scripts/enhancers/enhanceFacts.js | 4 +- src/ui/scripts/enhancers/enhanceLanguages.js | 2 +- src/ui/scripts/enhancers/enhancePages.js | 2 +- src/ui/scripts/enhancers/enhanceReferrers.js | 2 +- src/ui/scripts/enhancers/enhanceSizes.js | 2 +- src/ui/scripts/enhancers/enhanceSystems.js | 2 +- src/ui/scripts/enhancers/enhanceViews.js | 2 +- .../{useCardWidgets.js => useWidgets.js} | 14 ++++++- ...sForDomains.js => useWidgetsForDomains.js} | 4 +- src/ui/scripts/loaders/mergedFactsLoader.js | 3 +- src/ui/scripts/reducers/modals.js | 6 +-- src/ui/scripts/reducers/widgets.js | 19 ++++----- src/ui/scripts/utils/status.js | 2 +- 29 files changed, 116 insertions(+), 128 deletions(-) rename src/ui/scripts/components/cards/{CardFacts.js => CardFactsWidget.js} (53%) rename src/ui/scripts/hooks/{useCardWidgets.js => useWidgets.js} (73%) rename src/ui/scripts/hooks/{useCardWidgetsForDomains.js => useWidgetsForDomains.js} (83%) diff --git a/src/ui/scripts/actions/widgets.js b/src/ui/scripts/actions/widgets.js index 87cef2fe..7a03d0c5 100644 --- a/src/ui/scripts/actions/widgets.js +++ b/src/ui/scripts/actions/widgets.js @@ -6,9 +6,10 @@ export const SET_WIDGETS_END = Symbol() export const SET_WIDGETS_FETCHING = Symbol() export const SET_WIDGETS_ERROR = Symbol() -export const setWidgetsStart = (id, Renderer, variables) => ({ +export const setWidgetsStart = (id, value, Renderer, variables) => ({ type: SET_WIDGETS_START, id, + value, Renderer, variables }) @@ -45,8 +46,8 @@ export const fetchWidgets = signalHandler((signal) => (props, loaders) => async }).join('') loaders.forEach((loader) => { - const { id, Renderer, variables } = loader - dispatch(setWidgetsStart(id, Renderer, variables)) + const { id, Renderer, variables, enhancer } = loader + dispatch(setWidgetsStart(id, enhancer(), Renderer, variables)) }) try { @@ -62,7 +63,7 @@ export const fetchWidgets = signalHandler((signal) => (props, loaders) => async }) loaders.forEach((loader, index) => { - const { id, enhancer, selector } = loader + const { id, selector, enhancer } = loader const entryName = queryName(index) dispatch(setWidgetsEnd(id, enhancer(selector(data, entryName)))) }) @@ -77,35 +78,4 @@ export const fetchWidgets = signalHandler((signal) => (props, loaders) => async } -}) - -export const fetchWidget = signalHandler((signal) => (props, loader) => async (dispatch) => { - - const { id, Renderer, query, variables, selector, enhancer } = loader - - dispatch(setWidgetsStart(id, Renderer, variables)) - - try { - - const data = await api({ - query: ` - { - ${ query } - } - `, - props, - signal: signal(id) - }) - - dispatch(setWidgetsEnd(id, enhancer(selector(data)))) - - } catch (err) { - - if (err.name === 'AbortError') return - dispatch(setWidgetsFetching(id, false)) - if (err.name === 'HandledError') return - dispatch(setWidgetsError(id, err)) - - } - }) \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardFacts.js b/src/ui/scripts/components/cards/CardFactsWidget.js similarity index 53% rename from src/ui/scripts/components/cards/CardFacts.js rename to src/ui/scripts/components/cards/CardFactsWidget.js index dc1df870..e8434c79 100644 --- a/src/ui/scripts/components/cards/CardFacts.js +++ b/src/ui/scripts/components/cards/CardFactsWidget.js @@ -27,39 +27,48 @@ const Presentation = (props) => { const CardFacts = (props) => { + const { + activeVisitors, + averageViews, + averageDuration, + viewsToday, + viewsMonth, + viewsYear + } = props.widget.value + return ( h('div', { className: 'facts' }, h(Presentation, { headline: 'Active visitors', - value: props.items.activeVisitors, - text: pluralize([ 'visitors', 'visitor', 'visitors' ], props.items.activeVisitors) + value: activeVisitors, + text: pluralize([ 'visitors', 'visitor', 'visitors' ], activeVisitors) }), h(Presentation, { headline: 'Average views', - value: formatNumber(props.items.averageViews), + value: formatNumber(averageViews), text: 'per day' }), h(Presentation, { headline: 'Average duration', - value: formatDuration(props.items.averageDuration).value, - text: formatDuration(props.items.averageDuration).unit + value: formatDuration(averageDuration).value, + text: formatDuration(averageDuration).unit }), h(Presentation, { headline: 'Views today', - value: formatNumber(props.items.viewsToday), - text: pluralize([ 'views', 'view', 'views' ], props.items.viewsToday) + value: formatNumber(viewsToday), + text: pluralize([ 'views', 'view', 'views' ], viewsToday) }), h(Presentation, { headline: 'Views this month', - value: formatNumber(props.items.viewsMonth), - text: pluralize([ 'views', 'view', 'views' ], props.items.viewsMonth) + value: formatNumber(viewsMonth), + text: pluralize([ 'views', 'view', 'views' ], viewsMonth) }), h(Presentation, { headline: 'Views this year', - value: formatNumber(props.items.viewsYear), - text: pluralize([ 'views', 'view', 'views' ], props.items.viewsYear) + value: formatNumber(viewsYear), + text: pluralize([ 'views', 'view', 'views' ], viewsYear) }) ) ) @@ -67,14 +76,7 @@ const CardFacts = (props) => { } CardFacts.propTypes = { - items: PropTypes.exact({ - activeVisitors: PropTypes.number.isRequired, - averageViews: PropTypes.number.isRequired, - averageDuration: PropTypes.number.isRequired, - viewsToday: PropTypes.number.isRequired, - viewsMonth: PropTypes.number.isRequired, - viewsYear: PropTypes.number.isRequired - }).isRequired + widget: PropTypes.object.isRequired } export default CardFacts \ No newline at end of file diff --git a/src/ui/scripts/components/routes/RouteBrowsers.js b/src/ui/scripts/components/routes/RouteBrowsers.js index 9b272526..77ad42df 100644 --- a/src/ui/scripts/components/routes/RouteBrowsers.js +++ b/src/ui/scripts/components/routes/RouteBrowsers.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import browsersLoader from '../../loaders/browsersLoader' -import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' +import useWidgetsForDomains from '../../hooks/useWidgetsForDomains' const RouteBrowsers = (props) => { - return useCardWidgetsForDomains(props, browsersLoader, { + return useWidgetsForDomains(props, browsersLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.browsersType diff --git a/src/ui/scripts/components/routes/RouteDevices.js b/src/ui/scripts/components/routes/RouteDevices.js index 590efcd9..bd363412 100644 --- a/src/ui/scripts/components/routes/RouteDevices.js +++ b/src/ui/scripts/components/routes/RouteDevices.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import devicesLoader from '../../loaders/devicesLoader' -import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' +import useWidgetsForDomains from '../../hooks/useWidgetsForDomains' const RouteDevices = (props) => { - return useCardWidgetsForDomains(props, devicesLoader, { + return useWidgetsForDomains(props, devicesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.devicesType diff --git a/src/ui/scripts/components/routes/RouteDomain.js b/src/ui/scripts/components/routes/RouteDomain.js index 08de7d2f..1048adae 100644 --- a/src/ui/scripts/components/routes/RouteDomain.js +++ b/src/ui/scripts/components/routes/RouteDomain.js @@ -10,8 +10,9 @@ import { BROWSERS_TYPE_WITH_VERSION } from '../../../../constants/browsers' import { SIZES_TYPE_BROWSER_RESOLUTION } from '../../../../constants/sizes' import * as route from '../../constants/route' -import useCardWidgets from '../../hooks/useCardWidgets' +import useWidgets from '../../hooks/useWidgets' +import factsLoader from '../../loaders/factsLoader' import viewsLoader from '../../loaders/viewsLoader' import durationsLoader from '../../loaders/durationsLoader' import pagesLoader from '../../loaders/pagesLoader' @@ -22,14 +23,19 @@ import browsersLoader from '../../loaders/browsersLoader' import sizesLoader from '../../loaders/sizesLoader' import languagesLoader from '../../loaders/languagesLoader' -// TODO: Refactor facts -// import enhanceFacts from '../../enhancers/enhanceFacts' -// import CardFacts from '../cards/CardFacts' +import CardFactsWidget from '../cards/CardFactsWidget' const RouteDomain = (props) => { const domainId = props.route.params.domainId + const factsWidgetConfigs = useMemo(() => [ + { + WidgetComponent: CardFactsWidget, + loader: factsLoader(domainId, {}) + } + ], [ domainId ]) + const essentialWidgetConfigs = useMemo(() => [ { loader: viewsLoader(domainId, { @@ -72,7 +78,7 @@ const RouteDomain = (props) => { onMore: () => props.setRoute(route.ROUTE_REFERRERS) } } - ], []) + ], [ domainId ]) const detailedWidgetConfigs = useMemo(() => [ { @@ -129,16 +135,15 @@ const RouteDomain = (props) => { onMore: () => props.setRoute(route.ROUTE_LANGUAGES) } } - ], []) + ], [ domainId ]) - const renderedEssentialWidgets = useCardWidgets(props, essentialWidgetConfigs) - const renderedDetailedWidgets = useCardWidgets(props, detailedWidgetConfigs) + const renderedFactsWidgets = useWidgets(props, factsWidgetConfigs) + const renderedEssentialWidgets = useWidgets(props, essentialWidgetConfigs) + const renderedDetailedWidgets = useWidgets(props, detailedWidgetConfigs) return ( h(Fragment, {}, - // h(CardFacts, { - // items: enhanceFacts(selectOverviewValue.withoutType(props, domainId).facts) - // }), + renderedFactsWidgets, h('div', { className: 'content__spacer' }), renderedEssentialWidgets, h('div', { className: 'content__spacer' }), diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index de454f5b..66501404 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -2,8 +2,8 @@ import { createElement as h, Fragment, useMemo } from 'react' import mergedDurationsLoader from '../../loaders/mergedDurationsLoader' import durationsLoader from '../../loaders/durationsLoader' -import useCardWidgets from '../../hooks/useCardWidgets' -import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' +import useWidgets from '../../hooks/useWidgets' +import useWidgetsForDomains from '../../hooks/useWidgetsForDomains' const RouteDurations = (props) => { @@ -21,8 +21,8 @@ const RouteDurations = (props) => { }, [ props.filter.interval ]) - const renderedMergedWidgets = useCardWidgets(props, mergedWidgetConfigs) - const renderedDomainWidgets = useCardWidgetsForDomains(props, durationsLoader, { + const renderedMergedWidgets = useWidgets(props, mergedWidgetConfigs) + const renderedDomainWidgets = useWidgetsForDomains(props, durationsLoader, { interval: props.filter.interval }) diff --git a/src/ui/scripts/components/routes/RouteLanguages.js b/src/ui/scripts/components/routes/RouteLanguages.js index b14b249c..121c58ae 100644 --- a/src/ui/scripts/components/routes/RouteLanguages.js +++ b/src/ui/scripts/components/routes/RouteLanguages.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import languagesLoader from '../../loaders/languagesLoader' -import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' +import useWidgetsForDomains from '../../hooks/useWidgetsForDomains' const RouteLanguages = (props) => { - return useCardWidgetsForDomains(props, languagesLoader, { + return useWidgetsForDomains(props, languagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) diff --git a/src/ui/scripts/components/routes/RouteOverview.js b/src/ui/scripts/components/routes/RouteOverview.js index 23ea633c..b8ef7874 100644 --- a/src/ui/scripts/components/routes/RouteOverview.js +++ b/src/ui/scripts/components/routes/RouteOverview.js @@ -10,8 +10,9 @@ import { BROWSERS_TYPE_WITH_VERSION } from '../../../../constants/browsers' import { SIZES_TYPE_BROWSER_RESOLUTION } from '../../../../constants/sizes' import * as route from '../../constants/route' -import useCardWidgets from '../../hooks/useCardWidgets' +import useWidgets from '../../hooks/useWidgets' +import mergedFactsLoader from '../../loaders/mergedFactsLoader' import mergedViewsLoader from '../../loaders/mergedViewsLoader' import mergedDurationsLoader from '../../loaders/mergedDurationsLoader' import mergedPagesLoader from '../../loaders/mergedPagesLoader' @@ -22,12 +23,17 @@ import mergedBrowsersLoader from '../../loaders/mergedBrowsersLoader' import mergedSizesLoader from '../../loaders/mergedSizesLoader' import mergedLanguagesLoader from '../../loaders/mergedLanguagesLoader' -// TODO: Refactor facts -// import enhanceFacts from '../../enhancers/enhanceFacts' -// import CardFacts from '../cards/CardFacts' +import CardFactsWidget from '../cards/CardFactsWidget' const RouteOverview = (props) => { + const factsWidgetConfigs = useMemo(() => [ + { + WidgetComponent: CardFactsWidget, + loader: mergedFactsLoader({}) + } + ], []) + const essentialWidgetConfigs = useMemo(() => [ { loader: mergedViewsLoader({ @@ -129,14 +135,13 @@ const RouteOverview = (props) => { } ], []) - const renderedEssentialWidgets = useCardWidgets(props, essentialWidgetConfigs) - const renderedDetailedWidgets = useCardWidgets(props, detailedWidgetConfigs) + const renderedFactsWidgets = useWidgets(props, factsWidgetConfigs) + const renderedEssentialWidgets = useWidgets(props, essentialWidgetConfigs) + const renderedDetailedWidgets = useWidgets(props, detailedWidgetConfigs) return ( h(Fragment, {}, - // h(CardFacts, { - // items: enhanceFacts(selectOverviewValue.withoutType(props, domainId).facts) - // }), + renderedFactsWidgets, h('div', { className: 'content__spacer' }), renderedEssentialWidgets, h('div', { className: 'content__spacer' }), diff --git a/src/ui/scripts/components/routes/RoutePages.js b/src/ui/scripts/components/routes/RoutePages.js index 1d17feb5..09f981ee 100644 --- a/src/ui/scripts/components/routes/RoutePages.js +++ b/src/ui/scripts/components/routes/RoutePages.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import pagesLoader from '../../loaders/pagesLoader' -import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' +import useWidgetsForDomains from '../../hooks/useWidgetsForDomains' const RoutePages = (props) => { - return useCardWidgetsForDomains(props, pagesLoader, { + return useWidgetsForDomains(props, pagesLoader, { range: props.filter.range, sorting: props.filter.sorting }) diff --git a/src/ui/scripts/components/routes/RouteReferrers.js b/src/ui/scripts/components/routes/RouteReferrers.js index 988bc8a0..db3d2483 100644 --- a/src/ui/scripts/components/routes/RouteReferrers.js +++ b/src/ui/scripts/components/routes/RouteReferrers.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import referrersLoader from '../../loaders/referrersLoader' -import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' +import useWidgetsForDomains from '../../hooks/useWidgetsForDomains' const RouteReferrers = (props) => { - return useCardWidgetsForDomains(props, referrersLoader, { + return useWidgetsForDomains(props, referrersLoader, { range: props.filter.range, sorting: props.filter.sorting }) diff --git a/src/ui/scripts/components/routes/RouteSizes.js b/src/ui/scripts/components/routes/RouteSizes.js index e10302f2..7035c441 100644 --- a/src/ui/scripts/components/routes/RouteSizes.js +++ b/src/ui/scripts/components/routes/RouteSizes.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import sizesLoader from '../../loaders/sizesLoader' -import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' +import useWidgetsForDomains from '../../hooks/useWidgetsForDomains' const RouteSizes = (props) => { - return useCardWidgetsForDomains(props, sizesLoader, { + return useWidgetsForDomains(props, sizesLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.sizesType diff --git a/src/ui/scripts/components/routes/RouteSystems.js b/src/ui/scripts/components/routes/RouteSystems.js index dddf655c..24ce6c24 100644 --- a/src/ui/scripts/components/routes/RouteSystems.js +++ b/src/ui/scripts/components/routes/RouteSystems.js @@ -1,11 +1,11 @@ import { createElement as h } from 'react' import systemsLoader from '../../loaders/systemsLoader' -import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' +import useWidgetsForDomains from '../../hooks/useWidgetsForDomains' const RouteSystems = (props) => { - return useCardWidgetsForDomains(props, systemsLoader, { + return useWidgetsForDomains(props, systemsLoader, { range: props.filter.range, sorting: props.filter.sorting, type: props.filter.systemsType diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index 10ed5442..a2971ce6 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -3,8 +3,8 @@ import { createElement as h, Fragment, useMemo } from 'react' import { VIEWS_TYPE_UNIQUE, VIEWS_TYPE_TOTAL } from '../../../../constants/views' import mergedViewsLoader from '../../loaders/mergedViewsLoader' import viewsLoader from '../../loaders/viewsLoader' -import useCardWidgets from '../../hooks/useCardWidgets' -import useCardWidgetsForDomains from '../../hooks/useCardWidgetsForDomains' +import useWidgets from '../../hooks/useWidgets' +import useWidgetsForDomains from '../../hooks/useWidgetsForDomains' const RouteViews = (props) => { @@ -26,8 +26,8 @@ const RouteViews = (props) => { }, [ props.filter.interval, props.filter.viewsType ]) - const renderedMergedWidgets = useCardWidgets(props, mergedWidgetConfigs) - const renderedDomainWidgets = useCardWidgetsForDomains(props, viewsLoader, { + const renderedMergedWidgets = useWidgets(props, mergedWidgetConfigs) + const renderedDomainWidgets = useWidgetsForDomains(props, viewsLoader, { interval: props.filter.interval, type: props.filter.viewsType }) diff --git a/src/ui/scripts/enhancers/enhanceBrowsers.js b/src/ui/scripts/enhancers/enhanceBrowsers.js index 918fdf44..eaff151f 100644 --- a/src/ui/scripts/enhancers/enhanceBrowsers.js +++ b/src/ui/scripts/enhancers/enhanceBrowsers.js @@ -1,4 +1,4 @@ -export default (browsers) => { +export default (browsers = []) => { return browsers.map((browser) => ({ text: browser.id, diff --git a/src/ui/scripts/enhancers/enhanceDevices.js b/src/ui/scripts/enhancers/enhanceDevices.js index 2ebe57d9..7db275e3 100644 --- a/src/ui/scripts/enhancers/enhanceDevices.js +++ b/src/ui/scripts/enhancers/enhanceDevices.js @@ -1,4 +1,4 @@ -export default (devices) => { +export default (devices = []) => { return devices.map((device) => ({ text: device.id, diff --git a/src/ui/scripts/enhancers/enhanceDurations.js b/src/ui/scripts/enhancers/enhanceDurations.js index 7fa340d0..c8ee875b 100644 --- a/src/ui/scripts/enhancers/enhanceDurations.js +++ b/src/ui/scripts/enhancers/enhanceDurations.js @@ -1,7 +1,7 @@ import createArray from '../../../utils/createArray' // TODO: Avoid that this functions runs that may times -export default (durations, length) => createArray(length).map((_, index) => { +export default (durations = [], length) => createArray(length).map((_, index) => { const duration = durations[index] diff --git a/src/ui/scripts/enhancers/enhanceFacts.js b/src/ui/scripts/enhancers/enhanceFacts.js index 1c2b3294..506aae76 100644 --- a/src/ui/scripts/enhancers/enhanceFacts.js +++ b/src/ui/scripts/enhancers/enhanceFacts.js @@ -7,11 +7,11 @@ const defaults = { viewsYear: 0 } -export default (items) => { +export default (facts = {}) => { return { ...defaults, - ...items + ...facts } } \ No newline at end of file diff --git a/src/ui/scripts/enhancers/enhanceLanguages.js b/src/ui/scripts/enhancers/enhanceLanguages.js index c8f940cd..003926cf 100644 --- a/src/ui/scripts/enhancers/enhanceLanguages.js +++ b/src/ui/scripts/enhancers/enhanceLanguages.js @@ -1,4 +1,4 @@ -export default (languages) => { +export default (languages = []) => { return languages.map((language) => ({ text: language.id, diff --git a/src/ui/scripts/enhancers/enhancePages.js b/src/ui/scripts/enhancers/enhancePages.js index 556e2acd..b23c6360 100644 --- a/src/ui/scripts/enhancers/enhancePages.js +++ b/src/ui/scripts/enhancers/enhancePages.js @@ -1,4 +1,4 @@ -export default (pages) => { +export default (pages = []) => { return pages.map((page) => ({ url: new URL(page.id), diff --git a/src/ui/scripts/enhancers/enhanceReferrers.js b/src/ui/scripts/enhancers/enhanceReferrers.js index f2282d3c..0b9f713f 100644 --- a/src/ui/scripts/enhancers/enhanceReferrers.js +++ b/src/ui/scripts/enhancers/enhanceReferrers.js @@ -1,4 +1,4 @@ -export default (referrers) => { +export default (referrers = []) => { return referrers.map((referrer) => ({ url: new URL(referrer.id), diff --git a/src/ui/scripts/enhancers/enhanceSizes.js b/src/ui/scripts/enhancers/enhanceSizes.js index 2ef2f99d..f3f59432 100644 --- a/src/ui/scripts/enhancers/enhanceSizes.js +++ b/src/ui/scripts/enhancers/enhanceSizes.js @@ -1,4 +1,4 @@ -export default (sizes) => { +export default (sizes = []) => { return sizes.map((size) => ({ text: size.id, diff --git a/src/ui/scripts/enhancers/enhanceSystems.js b/src/ui/scripts/enhancers/enhanceSystems.js index 9c6fb768..e749d0c2 100644 --- a/src/ui/scripts/enhancers/enhanceSystems.js +++ b/src/ui/scripts/enhancers/enhanceSystems.js @@ -1,4 +1,4 @@ -export default (systems) => { +export default (systems = []) => { return systems.map((system) => ({ text: system.id, diff --git a/src/ui/scripts/enhancers/enhanceViews.js b/src/ui/scripts/enhancers/enhanceViews.js index 12d5dd00..0348ef45 100644 --- a/src/ui/scripts/enhancers/enhanceViews.js +++ b/src/ui/scripts/enhancers/enhanceViews.js @@ -1,7 +1,7 @@ import createArray from '../../../utils/createArray' // TODO: Avoid that this functions runs that may times -export default (views, length) => createArray(length).map((_, index) => { +export default (views = [], length) => createArray(length).map((_, index) => { const view = views[index] diff --git a/src/ui/scripts/hooks/useCardWidgets.js b/src/ui/scripts/hooks/useWidgets.js similarity index 73% rename from src/ui/scripts/hooks/useCardWidgets.js rename to src/ui/scripts/hooks/useWidgets.js index 74bf770b..ea06db19 100644 --- a/src/ui/scripts/hooks/useCardWidgets.js +++ b/src/ui/scripts/hooks/useWidgets.js @@ -4,6 +4,11 @@ import { initialSubState } from '../reducers/widgets' import CardWidget from '../components/cards/CardWidget' +const defaultWidgetConfig = { + WidgetComponent: CardWidget, + additionalProps: {} +} + export default (props, widgetConfigs = []) => { const [ widgetIds, setWidgetIds ] = useState([]) @@ -27,10 +32,15 @@ export default (props, widgetConfigs = []) => { (widgetId, index) => { const widgetData = props.widgets.value[widgetId] || initialSubState() - return h(CardWidget, { + const widgetConfig = { + ...defaultWidgetConfig, + ...widgetConfigs[index] + } + + return h(widgetConfig.WidgetComponent, { key: widgetId, widget: widgetData, - ...widgetConfigs[index].additionalProps + ...widgetConfig.additionalProps }) } ) diff --git a/src/ui/scripts/hooks/useCardWidgetsForDomains.js b/src/ui/scripts/hooks/useWidgetsForDomains.js similarity index 83% rename from src/ui/scripts/hooks/useCardWidgetsForDomains.js rename to src/ui/scripts/hooks/useWidgetsForDomains.js index ee71f649..9a4cb5bd 100644 --- a/src/ui/scripts/hooks/useCardWidgetsForDomains.js +++ b/src/ui/scripts/hooks/useWidgetsForDomains.js @@ -1,6 +1,6 @@ import { createElement as h, useMemo } from 'react' -import useCardWidgets from './useCardWidgets' +import useWidgets from './useWidgets' import domainRoute from '../utils/domainRoute' export default (props, createLoader, opts) => { @@ -17,6 +17,6 @@ export default (props, createLoader, opts) => { }, [ props.domains.value, ...Object.values(opts) ]) - return useCardWidgets(props, widgetConfigs) + return useWidgets(props, widgetConfigs) } \ No newline at end of file diff --git a/src/ui/scripts/loaders/mergedFactsLoader.js b/src/ui/scripts/loaders/mergedFactsLoader.js index 8a4c3c7e..381c5af7 100644 --- a/src/ui/scripts/loaders/mergedFactsLoader.js +++ b/src/ui/scripts/loaders/mergedFactsLoader.js @@ -1,6 +1,5 @@ import { createElement as h } from 'react' -import RendererList from '../components/renderers/RendererList' import enhanceFacts from '../enhancers/enhanceFacts' import createWidgetId from '../utils/createWidgetId' @@ -25,7 +24,7 @@ export default (opts) => { return { id, - Renderer: RendererList, + Renderer: undefined, query, variables, selector, diff --git a/src/ui/scripts/reducers/modals.js b/src/ui/scripts/reducers/modals.js index b017c643..604cdad5 100644 --- a/src/ui/scripts/reducers/modals.js +++ b/src/ui/scripts/reducers/modals.js @@ -19,13 +19,9 @@ export const initialSubState = () => ({ export default produce((draft, action) => { - const hasModalId = () => action.modalId != null - const hasModalValue = () => draft.value[action.modalId] != null - - if (hasModalId() === true && hasModalValue() === false) draft.value[action.modalId] = initialSubState() - switch (action.type) { case SET_MODALS_STATE: + draft.value[action.modalId] = draft.value[action.modalId] || initialSubState() draft.value[action.modalId].id = action.modalId draft.value[action.modalId].type = action.payload.type draft.value[action.modalId].props = action.payload.props || initialSubState().props diff --git a/src/ui/scripts/reducers/widgets.js b/src/ui/scripts/reducers/widgets.js index f47092ad..ac326a08 100644 --- a/src/ui/scripts/reducers/widgets.js +++ b/src/ui/scripts/reducers/widgets.js @@ -12,7 +12,7 @@ export const initialState = () => ({ }) export const initialSubState = () => ({ - value: [], + value: undefined, Renderer: () => null, variables: {}, fetching: false, @@ -21,19 +21,20 @@ export const initialSubState = () => ({ export default produce((draft, action) => { - const hasId = () => action.id != null - const hasValue = () => draft.value[action.id] != null - - if (hasId() === true && hasValue() === false) draft.value[action.id] = initialSubState() - switch (action.type) { case SET_WIDGETS_START: - draft.value[action.id].Renderer = action.Renderer || initialSubState().Renderer - draft.value[action.id].variables = action.variables || initialSubState().variables + // Initialize when id is unknown + draft.value[action.id] = draft.value[action.id] || initialSubState() + // Reuse existing value when available + const value = draft.value[action.id].value == null ? action.value : draft.value[action.id].value + draft.value[action.id].value = value + // Set remaining data + draft.value[action.id].Renderer = action.Renderer + draft.value[action.id].variables = action.variables draft.value[action.id].fetching = true break case SET_WIDGETS_END: - draft.value[action.id].value = action.value || initialSubState().value + draft.value[action.id].value = action.value draft.value[action.id].fetching = false break case SET_WIDGETS_FETCHING: diff --git a/src/ui/scripts/utils/status.js b/src/ui/scripts/utils/status.js index d5c44d48..ac9fef07 100644 --- a/src/ui/scripts/utils/status.js +++ b/src/ui/scripts/utils/status.js @@ -1,6 +1,6 @@ export default (entries, loading) => { - const isEmpty = entries.length === 0 + const isEmpty = entries == null || entries.length === 0 const isStale = isEmpty === false && loading === true const isLoading = isEmpty === true && loading === true From 2dbb72516303b53e6bb194856e36885bc44e9064 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 19:50:14 +0100 Subject: [PATCH 081/208] Add comment and remove resolved todos --- src/ui/scripts/enhancers/enhanceDurations.js | 1 - src/ui/scripts/enhancers/enhanceViews.js | 1 - src/ui/scripts/reducers/modals.js | 2 ++ 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/enhancers/enhanceDurations.js b/src/ui/scripts/enhancers/enhanceDurations.js index c8ee875b..6590ccf5 100644 --- a/src/ui/scripts/enhancers/enhanceDurations.js +++ b/src/ui/scripts/enhancers/enhanceDurations.js @@ -1,6 +1,5 @@ import createArray from '../../../utils/createArray' -// TODO: Avoid that this functions runs that may times export default (durations = [], length) => createArray(length).map((_, index) => { const duration = durations[index] diff --git a/src/ui/scripts/enhancers/enhanceViews.js b/src/ui/scripts/enhancers/enhanceViews.js index 0348ef45..4b02295f 100644 --- a/src/ui/scripts/enhancers/enhanceViews.js +++ b/src/ui/scripts/enhancers/enhanceViews.js @@ -1,6 +1,5 @@ import createArray from '../../../utils/createArray' -// TODO: Avoid that this functions runs that may times export default (views = [], length) => createArray(length).map((_, index) => { const view = views[index] diff --git a/src/ui/scripts/reducers/modals.js b/src/ui/scripts/reducers/modals.js index 604cdad5..4a392be4 100644 --- a/src/ui/scripts/reducers/modals.js +++ b/src/ui/scripts/reducers/modals.js @@ -21,7 +21,9 @@ export default produce((draft, action) => { switch (action.type) { case SET_MODALS_STATE: + // Initialize when id is unknown draft.value[action.modalId] = draft.value[action.modalId] || initialSubState() + // Set remaining data draft.value[action.modalId].id = action.modalId draft.value[action.modalId].type = action.payload.type draft.value[action.modalId].props = action.payload.props || initialSubState().props From 2604fa7f220125752f362653e71c6afbb63a7af8 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 19:51:28 +0100 Subject: [PATCH 082/208] Only store token id --- src/ui/scripts/actions/token.js | 5 ++--- src/ui/scripts/components/Main.js | 2 +- src/ui/scripts/reducers/token.js | 2 +- src/ui/scripts/utils/api.js | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/ui/scripts/actions/token.js b/src/ui/scripts/actions/token.js index bba498da..38b2652a 100644 --- a/src/ui/scripts/actions/token.js +++ b/src/ui/scripts/actions/token.js @@ -53,8 +53,7 @@ export const addToken = signalHandler((signal) => (props, state) => async (dispa signal: signal() }) - // TODO: Maybe just store the id instead of the payload - dispatch(setTokenEnd(data.createToken.payload)) + dispatch(setTokenEnd(data.createToken.payload.id)) } catch (err) { @@ -82,7 +81,7 @@ export const deleteToken = signalHandler((signal) => (props) => async (dispatch) } `, variables: { - id: props.token.value.id + id: props.token.value }, props, signal: signal() diff --git a/src/ui/scripts/components/Main.js b/src/ui/scripts/components/Main.js index 516ee240..6179c71c 100644 --- a/src/ui/scripts/components/Main.js +++ b/src/ui/scripts/components/Main.js @@ -24,7 +24,7 @@ const Main = (props) => { const unknownErrors = props.errors.filter(isUnknownError) const hasError = unknownErrors.length !== 0 - const hasToken = props.token.value.id != null + const hasToken = props.token.value != null const showOverlayFailure = hasError === true const showOverlayLogin = hasError === false && hasToken === false diff --git a/src/ui/scripts/reducers/token.js b/src/ui/scripts/reducers/token.js index 92951e5e..48226e75 100644 --- a/src/ui/scripts/reducers/token.js +++ b/src/ui/scripts/reducers/token.js @@ -8,7 +8,7 @@ import { } from '../actions' export const initialState = () => ({ - value: {}, + value: undefined, fetching: false, error: undefined }) diff --git a/src/ui/scripts/utils/api.js b/src/ui/scripts/utils/api.js index fa4da36c..5ce1224f 100644 --- a/src/ui/scripts/utils/api.js +++ b/src/ui/scripts/utils/api.js @@ -7,7 +7,7 @@ export default async ({ query, variables, props, signal }) => { try { const headers = new Headers() - const token = props.token.value.id + const token = props.token.value headers.append('Content-Type', 'application/json') headers.append('Time-Zone', userTimeZone) From f340236d7901491c77d9142e7db966b1e9887fb5 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 20:00:26 +0100 Subject: [PATCH 083/208] Avoid empty requests --- src/ui/scripts/hooks/useWidgets.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/hooks/useWidgets.js b/src/ui/scripts/hooks/useWidgets.js index ea06db19..1c583fde 100644 --- a/src/ui/scripts/hooks/useWidgets.js +++ b/src/ui/scripts/hooks/useWidgets.js @@ -23,8 +23,12 @@ export default (props, widgetConfigs = []) => { loader.id ) - props.fetchWidgets(props, loaders) - setWidgetIds(widgetIds) + // Only fetch widgets when there's something to load. + // Empty requests are forbidden. + if (loaders.length > 0) { + props.fetchWidgets(props, loaders) + setWidgetIds(widgetIds) + } }, [ widgetConfigs ]) From 7a19b2f1412617a5e7c2e43a0889b191c9a0c26f Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 20:06:24 +0100 Subject: [PATCH 084/208] Fix ids --- src/ui/scripts/loaders/mergedFactsLoader.js | 2 +- src/ui/scripts/loaders/mergedLanguagesLoader.js | 2 +- src/ui/scripts/loaders/mergedPagesLoader.js | 2 +- src/ui/scripts/loaders/mergedReferrersLoader.js | 2 +- src/ui/scripts/loaders/mergedSizesLoader.js | 2 +- src/ui/scripts/loaders/mergedSystemsLoader.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/scripts/loaders/mergedFactsLoader.js b/src/ui/scripts/loaders/mergedFactsLoader.js index 381c5af7..7477dc10 100644 --- a/src/ui/scripts/loaders/mergedFactsLoader.js +++ b/src/ui/scripts/loaders/mergedFactsLoader.js @@ -5,7 +5,7 @@ import createWidgetId from '../utils/createWidgetId' export default (opts) => { - const id = createWidgetId('fetchFacts', undefined, opts) + const id = createWidgetId('fetchMergedFacts', undefined, opts) const query = ` facts { diff --git a/src/ui/scripts/loaders/mergedLanguagesLoader.js b/src/ui/scripts/loaders/mergedLanguagesLoader.js index feb2fa24..3cac0c1f 100644 --- a/src/ui/scripts/loaders/mergedLanguagesLoader.js +++ b/src/ui/scripts/loaders/mergedLanguagesLoader.js @@ -6,7 +6,7 @@ import createWidgetId from '../utils/createWidgetId' export default (opts) => { - const id = createWidgetId('fetchLanguages', undefined, opts) + const id = createWidgetId('fetchMergedLanguages', undefined, opts) const query = ` statistics { diff --git a/src/ui/scripts/loaders/mergedPagesLoader.js b/src/ui/scripts/loaders/mergedPagesLoader.js index 240433c5..563c4810 100644 --- a/src/ui/scripts/loaders/mergedPagesLoader.js +++ b/src/ui/scripts/loaders/mergedPagesLoader.js @@ -6,7 +6,7 @@ import createWidgetId from '../utils/createWidgetId' export default (opts) => { - const id = createWidgetId('fetchPages', undefined, opts) + const id = createWidgetId('fetchMergedPages', undefined, opts) const query = ` statistics { diff --git a/src/ui/scripts/loaders/mergedReferrersLoader.js b/src/ui/scripts/loaders/mergedReferrersLoader.js index 16cbcfcd..f536489e 100644 --- a/src/ui/scripts/loaders/mergedReferrersLoader.js +++ b/src/ui/scripts/loaders/mergedReferrersLoader.js @@ -6,7 +6,7 @@ import createWidgetId from '../utils/createWidgetId' export default (opts) => { - const id = createWidgetId('fetchReferrers', undefined, opts) + const id = createWidgetId('fetchMergedReferrers', undefined, opts) const query = ` statistics { diff --git a/src/ui/scripts/loaders/mergedSizesLoader.js b/src/ui/scripts/loaders/mergedSizesLoader.js index 749daba4..331982d4 100644 --- a/src/ui/scripts/loaders/mergedSizesLoader.js +++ b/src/ui/scripts/loaders/mergedSizesLoader.js @@ -6,7 +6,7 @@ import createWidgetId from '../utils/createWidgetId' export default (opts) => { - const id = createWidgetId('fetchSizes', undefined, opts) + const id = createWidgetId('fetchMergedSizes', undefined, opts) const query = ` statistics { diff --git a/src/ui/scripts/loaders/mergedSystemsLoader.js b/src/ui/scripts/loaders/mergedSystemsLoader.js index 098ebdd2..8159e9e9 100644 --- a/src/ui/scripts/loaders/mergedSystemsLoader.js +++ b/src/ui/scripts/loaders/mergedSystemsLoader.js @@ -6,7 +6,7 @@ import createWidgetId from '../utils/createWidgetId' export default (opts) => { - const id = createWidgetId('fetchSystems', undefined, opts) + const id = createWidgetId('fetchMergedSystems', undefined, opts) const query = ` statistics { From 859f241c0d7381def182af5a39cb4a7a0d88bc40 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 12 Dec 2020 20:42:30 +0100 Subject: [PATCH 085/208] Reuse domainId when possible --- src/ui/scripts/hooks/useWidgets.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/scripts/hooks/useWidgets.js b/src/ui/scripts/hooks/useWidgets.js index 1c583fde..3c95cf69 100644 --- a/src/ui/scripts/hooks/useWidgets.js +++ b/src/ui/scripts/hooks/useWidgets.js @@ -35,6 +35,7 @@ export default (props, widgetConfigs = []) => { return widgetIds.map( (widgetId, index) => { const widgetData = props.widgets.value[widgetId] || initialSubState() + const widgetKey = widgetData.variables.domainId || widgetId const widgetConfig = { ...defaultWidgetConfig, @@ -42,7 +43,7 @@ export default (props, widgetConfigs = []) => { } return h(widgetConfig.WidgetComponent, { - key: widgetId, + key: widgetKey, widget: widgetData, ...widgetConfig.additionalProps }) From ab0922e132557c68848f4b40448966b071fd441b Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 12:12:51 +0100 Subject: [PATCH 086/208] Batch updates to reduce repaints --- src/ui/scripts/actions/widgets.js | 18 +++++++++--------- src/ui/scripts/utils/batchDispatch.js | 7 +++++++ 2 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 src/ui/scripts/utils/batchDispatch.js diff --git a/src/ui/scripts/actions/widgets.js b/src/ui/scripts/actions/widgets.js index 7a03d0c5..57ff9b6a 100644 --- a/src/ui/scripts/actions/widgets.js +++ b/src/ui/scripts/actions/widgets.js @@ -1,5 +1,6 @@ import api from '../utils/api' import signalHandler from '../utils/signalHandler' +import batchDispatch from '../utils/batchDispatch' export const SET_WIDGETS_START = Symbol() export const SET_WIDGETS_END = Symbol() @@ -45,10 +46,10 @@ export const fetchWidgets = signalHandler((signal) => (props, loaders) => async return `${ queryName(index) }: ${ query }` }).join('') - loaders.forEach((loader) => { + batchDispatch(dispatch, loaders.map((loader) => { const { id, Renderer, variables, enhancer } = loader - dispatch(setWidgetsStart(id, enhancer(), Renderer, variables)) - }) + return setWidgetsStart(id, enhancer(), Renderer, variables) + })) try { @@ -62,19 +63,18 @@ export const fetchWidgets = signalHandler((signal) => (props, loaders) => async signal: signal(id) }) - loaders.forEach((loader, index) => { + batchDispatch(dispatch, loaders.map((loader, index) => { const { id, selector, enhancer } = loader const entryName = queryName(index) - dispatch(setWidgetsEnd(id, enhancer(selector(data, entryName)))) - }) - + return setWidgetsEnd(id, enhancer(selector(data, entryName))) + })) } catch (err) { if (err.name === 'AbortError') return - loaders.forEach((loader) => dispatch(setWidgetsFetching(loader.id, false))) + batchDispatch(dispatch, loaders.map((loader) => setWidgetsFetching(loader.id, false))) if (err.name === 'HandledError') return - loaders.forEach((loader) => dispatch(setWidgetsError(loader.id, err))) + batchDispatch(dispatch, loaders.map((loader) => setWidgetsError(loader.id, err))) } diff --git a/src/ui/scripts/utils/batchDispatch.js b/src/ui/scripts/utils/batchDispatch.js new file mode 100644 index 00000000..cdbc1f93 --- /dev/null +++ b/src/ui/scripts/utils/batchDispatch.js @@ -0,0 +1,7 @@ +import { batch } from 'react-redux' + +export default (dispatch, actions) => { + batch(() => { + actions.forEach((action) => dispatch(action)) + }) +} \ No newline at end of file From 4c0c5e6c04f75b4facf6c550cb85276d50b0c533 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 12:41:03 +0100 Subject: [PATCH 087/208] Headline must be a string --- src/ui/scripts/components/cards/CardWidget.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/ui/scripts/components/cards/CardWidget.js b/src/ui/scripts/components/cards/CardWidget.js index 70d42454..6f4cdbd2 100644 --- a/src/ui/scripts/components/cards/CardWidget.js +++ b/src/ui/scripts/components/cards/CardWidget.js @@ -16,10 +16,6 @@ const CardWidget = (props) => { isLoading } = status(props.widget.value, props.widget.fetching) - const headline = typeof props.headline === 'function' ? - props.headline(props.widget) : - props.headline - // Use thin space as initial value to avoid that the label changes the height once rendered const [ textLabel, setTextLabel ] = useState(' ') @@ -54,7 +50,7 @@ const CardWidget = (props) => { type: 'h2', size: 'medium', onClick: props.onMore - }, headline), + }, props.headline), h(Text, { type: 'div', spacing: false @@ -68,10 +64,7 @@ const CardWidget = (props) => { CardWidget.propTypes = { wide: PropTypes.bool, - headline: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.func - ]).isRequired, + headline: PropTypes.string.isRequired, widget: PropTypes.object.isRequired, onMore: PropTypes.func } From 37ea0a68a836c15ad4fb8a4740ade9765d926e92 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 12:57:30 +0100 Subject: [PATCH 088/208] Domain route with 14 days views and durations --- src/ui/scripts/components/routes/RouteDomain.js | 6 ++++-- src/ui/scripts/components/routes/RouteDurations.js | 6 ++++-- src/ui/scripts/components/routes/RouteOverview.js | 6 ++++-- src/ui/scripts/components/routes/RouteViews.js | 6 ++++-- src/ui/scripts/loaders/durationsLoader.js | 7 ++++--- src/ui/scripts/loaders/mergedDurationsLoader.js | 7 ++++--- src/ui/scripts/loaders/mergedViewsLoader.js | 7 ++++--- src/ui/scripts/loaders/viewsLoader.js | 7 ++++--- 8 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/ui/scripts/components/routes/RouteDomain.js b/src/ui/scripts/components/routes/RouteDomain.js index 1048adae..4fcf561d 100644 --- a/src/ui/scripts/components/routes/RouteDomain.js +++ b/src/ui/scripts/components/routes/RouteDomain.js @@ -40,7 +40,8 @@ const RouteDomain = (props) => { { loader: viewsLoader(domainId, { interval: INTERVALS_DAILY, - type: VIEWS_TYPE_UNIQUE + type: VIEWS_TYPE_UNIQUE, + limit: 14 }), additionalProps: { wide: true, @@ -50,7 +51,8 @@ const RouteDomain = (props) => { }, { loader: durationsLoader(domainId, { - interval: INTERVALS_DAILY + interval: INTERVALS_DAILY, + limit: 14 }), additionalProps: { wide: true, diff --git a/src/ui/scripts/components/routes/RouteDurations.js b/src/ui/scripts/components/routes/RouteDurations.js index 66501404..b0942faf 100644 --- a/src/ui/scripts/components/routes/RouteDurations.js +++ b/src/ui/scripts/components/routes/RouteDurations.js @@ -11,7 +11,8 @@ const RouteDurations = (props) => { return [{ loader: mergedDurationsLoader({ - interval: props.filter.interval + interval: props.filter.interval, + limit: 14 }), additionalProps: { wide: true, @@ -23,7 +24,8 @@ const RouteDurations = (props) => { const renderedMergedWidgets = useWidgets(props, mergedWidgetConfigs) const renderedDomainWidgets = useWidgetsForDomains(props, durationsLoader, { - interval: props.filter.interval + interval: props.filter.interval, + limit: 7 }) return ( diff --git a/src/ui/scripts/components/routes/RouteOverview.js b/src/ui/scripts/components/routes/RouteOverview.js index b8ef7874..be8de981 100644 --- a/src/ui/scripts/components/routes/RouteOverview.js +++ b/src/ui/scripts/components/routes/RouteOverview.js @@ -38,7 +38,8 @@ const RouteOverview = (props) => { { loader: mergedViewsLoader({ interval: INTERVALS_DAILY, - type: VIEWS_TYPE_UNIQUE + type: VIEWS_TYPE_UNIQUE, + limit: 14 }), additionalProps: { wide: true, @@ -48,7 +49,8 @@ const RouteOverview = (props) => { }, { loader: mergedDurationsLoader({ - interval: INTERVALS_DAILY + interval: INTERVALS_DAILY, + limit: 14 }), additionalProps: { wide: true, diff --git a/src/ui/scripts/components/routes/RouteViews.js b/src/ui/scripts/components/routes/RouteViews.js index a2971ce6..d37f4e08 100644 --- a/src/ui/scripts/components/routes/RouteViews.js +++ b/src/ui/scripts/components/routes/RouteViews.js @@ -13,7 +13,8 @@ const RouteViews = (props) => { return [{ loader: mergedViewsLoader({ interval: props.filter.interval, - type: props.filter.viewsType + type: props.filter.viewsType, + limit: 14 }), additionalProps: { wide: true, @@ -29,7 +30,8 @@ const RouteViews = (props) => { const renderedMergedWidgets = useWidgets(props, mergedWidgetConfigs) const renderedDomainWidgets = useWidgetsForDomains(props, viewsLoader, { interval: props.filter.interval, - type: props.filter.viewsType + type: props.filter.viewsType, + limit: 7 }) return ( diff --git a/src/ui/scripts/loaders/durationsLoader.js b/src/ui/scripts/loaders/durationsLoader.js index 1c3bb266..29252add 100644 --- a/src/ui/scripts/loaders/durationsLoader.js +++ b/src/ui/scripts/loaders/durationsLoader.js @@ -12,7 +12,7 @@ export default (domainId, opts) => { const query = ` domain(id: "${ domainId }") { statistics { - durations(interval: ${ opts.interval }, limit: 7) { + durations(interval: ${ opts.interval }, limit: ${ opts.limit }) { id count } @@ -22,7 +22,8 @@ export default (domainId, opts) => { const variables = { domainId, - interval: opts.interval + interval: opts.interval, + limit: opts.limit } const selector = (data, entryName = 'domain') => data[entryName].statistics.durations @@ -36,7 +37,7 @@ export default (domainId, opts) => { query, variables, selector, - enhancer: (durations) => enhanceDurations(durations, 7) + enhancer: (durations) => enhanceDurations(durations, opts.limit) } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/mergedDurationsLoader.js b/src/ui/scripts/loaders/mergedDurationsLoader.js index 2783337b..46c621bb 100644 --- a/src/ui/scripts/loaders/mergedDurationsLoader.js +++ b/src/ui/scripts/loaders/mergedDurationsLoader.js @@ -11,7 +11,7 @@ export default (opts) => { const query = ` statistics { - durations(interval: ${ opts.interval }) { + durations(interval: ${ opts.interval }, limit: ${ opts.limit }) { id count } @@ -19,7 +19,8 @@ export default (opts) => { ` const variables = { - interval: opts.interval + interval: opts.interval, + limit: opts.limit } const selector = (data, entryName = 'statistics') => data[entryName].durations @@ -33,7 +34,7 @@ export default (opts) => { query, variables, selector, - enhancer: (durations) => enhanceDurations(durations, 14) + enhancer: (durations) => enhanceDurations(durations, opts.limit) } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/mergedViewsLoader.js b/src/ui/scripts/loaders/mergedViewsLoader.js index f4b0fd77..6504945a 100644 --- a/src/ui/scripts/loaders/mergedViewsLoader.js +++ b/src/ui/scripts/loaders/mergedViewsLoader.js @@ -11,7 +11,7 @@ export default (opts) => { const query = ` statistics { - views(interval: ${ opts.interval }, type: ${ opts.type }) { + views(interval: ${ opts.interval }, type: ${ opts.type }, limit: ${ opts.limit }) { id count } @@ -20,7 +20,8 @@ export default (opts) => { const variables = { interval: opts.interval, - type: opts.type + type: opts.type, + limit: opts.limit } const selector = (data, entryName = 'statistics') => data[entryName].views @@ -34,7 +35,7 @@ export default (opts) => { query, variables, selector, - enhancer: (views) => enhanceViews(views, 14) + enhancer: (views) => enhanceViews(views, opts.limit) } } \ No newline at end of file diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js index 4be51dcd..d44b207b 100644 --- a/src/ui/scripts/loaders/viewsLoader.js +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -12,7 +12,7 @@ export default (domainId, opts) => { const query = ` domain(id: "${ domainId }") { statistics { - views(interval: ${ opts.interval }, type: ${ opts.type }, limit: 7) { + views(interval: ${ opts.interval }, type: ${ opts.type }, limit: ${ opts.limit }) { id count } @@ -23,7 +23,8 @@ export default (domainId, opts) => { const variables = { domainId, type: opts.type, - interval: opts.interval + interval: opts.interval, + limit: opts.limit } const selector = (data, entryName = 'domain') => data[entryName].statistics.views @@ -37,7 +38,7 @@ export default (domainId, opts) => { query, variables, selector, - enhancer: (views) => enhanceViews(views, 7) + enhancer: (views) => enhanceViews(views, opts.limit) } } \ No newline at end of file From e90ab6504bb03741ee3ac5e3027391b8273e0473 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 13:02:46 +0100 Subject: [PATCH 089/208] Fix duplicate key --- src/ui/scripts/hooks/useWidgets.js | 3 +-- src/ui/scripts/hooks/useWidgetsForDomains.js | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/hooks/useWidgets.js b/src/ui/scripts/hooks/useWidgets.js index 3c95cf69..a36f8926 100644 --- a/src/ui/scripts/hooks/useWidgets.js +++ b/src/ui/scripts/hooks/useWidgets.js @@ -35,7 +35,6 @@ export default (props, widgetConfigs = []) => { return widgetIds.map( (widgetId, index) => { const widgetData = props.widgets.value[widgetId] || initialSubState() - const widgetKey = widgetData.variables.domainId || widgetId const widgetConfig = { ...defaultWidgetConfig, @@ -43,7 +42,7 @@ export default (props, widgetConfigs = []) => { } return h(widgetConfig.WidgetComponent, { - key: widgetKey, + key: widgetData.key || widgetId, widget: widgetData, ...widgetConfig.additionalProps }) diff --git a/src/ui/scripts/hooks/useWidgetsForDomains.js b/src/ui/scripts/hooks/useWidgetsForDomains.js index 9a4cb5bd..5bf8fcc9 100644 --- a/src/ui/scripts/hooks/useWidgetsForDomains.js +++ b/src/ui/scripts/hooks/useWidgetsForDomains.js @@ -8,6 +8,7 @@ export default (props, createLoader, opts) => { const widgetConfigs = useMemo(() => { return props.domains.value.map((domain) => ({ + key: domain.id, loader: createLoader(domain.id, opts), additionalProps: { headline: domain.title, From 5327c0616b332bcf578cb4c1a2e02b3dd4ce8d39 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 13:17:36 +0100 Subject: [PATCH 090/208] Avoid rendering zero widgets Happend because of useEffect and an empty useState --- src/ui/scripts/actions/widgets.js | 3 +++ src/ui/scripts/hooks/useWidgets.js | 38 ++++++++++++++++-------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/ui/scripts/actions/widgets.js b/src/ui/scripts/actions/widgets.js index 57ff9b6a..2c8ac845 100644 --- a/src/ui/scripts/actions/widgets.js +++ b/src/ui/scripts/actions/widgets.js @@ -35,6 +35,9 @@ export const setWidgetsError = (id, payload) => ({ export const fetchWidgets = signalHandler((signal) => (props, loaders) => async (dispatch) => { + // Only fetch widgets when there's something to load as empty requests are forbidden + if (loaders.length === 0) return + const id = loaders.map((loader) => loader.id).join('') // Generate an unique name for every query diff --git a/src/ui/scripts/hooks/useWidgets.js b/src/ui/scripts/hooks/useWidgets.js index a36f8926..3e93ab65 100644 --- a/src/ui/scripts/hooks/useWidgets.js +++ b/src/ui/scripts/hooks/useWidgets.js @@ -1,4 +1,4 @@ -import { createElement as h, useState, useEffect } from 'react' +import { createElement as h, useMemo, useEffect } from 'react' import { initialSubState } from '../reducers/widgets' @@ -11,30 +11,32 @@ const defaultWidgetConfig = { export default (props, widgetConfigs = []) => { - const [ widgetIds, setWidgetIds ] = useState([]) + const loaders = useMemo(() => { - useEffect(() => { - - const loaders = widgetConfigs.map((widgetConfig) => + return widgetConfigs.map((widgetConfig) => widgetConfig.loader ) - const widgetIds = loaders.map((loader) => - loader.id - ) + }, [ widgetConfigs ]) - // Only fetch widgets when there's something to load. - // Empty requests are forbidden. - if (loaders.length > 0) { - props.fetchWidgets(props, loaders) - setWidgetIds(widgetIds) - } + useEffect(() => { - }, [ widgetConfigs ]) + props.fetchWidgets(props, loaders) + + }, [ loaders ]) - return widgetIds.map( - (widgetId, index) => { - const widgetData = props.widgets.value[widgetId] || initialSubState() + return loaders.map( + (loader, index) => { + const widgetId = loader.id + + // Ensure that the data is never empty, even when the widget is not ready or + // still loading. Also initialize the value with the correct type using by + // calling the enhancer without parameters. + const widgetData = { + ...initialSubState(), + value: loader.enhancer(), + ...(props.widgets.value[widgetId] || {}) + } const widgetConfig = { ...defaultWidgetConfig, From aefb502666ef796af3ddcac53fab1ef4ee0e51b3 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 13:28:50 +0100 Subject: [PATCH 091/208] Use correct key --- src/ui/scripts/hooks/useWidgets.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/scripts/hooks/useWidgets.js b/src/ui/scripts/hooks/useWidgets.js index 3e93ab65..896bedb6 100644 --- a/src/ui/scripts/hooks/useWidgets.js +++ b/src/ui/scripts/hooks/useWidgets.js @@ -44,7 +44,7 @@ export default (props, widgetConfigs = []) => { } return h(widgetConfig.WidgetComponent, { - key: widgetData.key || widgetId, + key: widgetConfig.key || widgetId, widget: widgetData, ...widgetConfig.additionalProps }) From aa3c929536adca193841d95438a700d7baada7a7 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 14:02:14 +0100 Subject: [PATCH 092/208] Remove Renderer and variables from the store and ensure that widget never render without a Renderer or variables --- src/ui/scripts/actions/widgets.js | 10 ++-- .../scripts/components/routes/RouteDomain.js | 10 ++++ src/ui/scripts/hooks/useWidgets.js | 46 +++++++++++++------ src/ui/scripts/reducers/widgets.js | 4 -- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/ui/scripts/actions/widgets.js b/src/ui/scripts/actions/widgets.js index 2c8ac845..e024c4b0 100644 --- a/src/ui/scripts/actions/widgets.js +++ b/src/ui/scripts/actions/widgets.js @@ -7,12 +7,10 @@ export const SET_WIDGETS_END = Symbol() export const SET_WIDGETS_FETCHING = Symbol() export const SET_WIDGETS_ERROR = Symbol() -export const setWidgetsStart = (id, value, Renderer, variables) => ({ +export const setWidgetsStart = (id, value) => ({ type: SET_WIDGETS_START, id, - value, - Renderer, - variables + value }) export const setWidgetsEnd = (id, value) => ({ @@ -50,8 +48,8 @@ export const fetchWidgets = signalHandler((signal) => (props, loaders) => async }).join('') batchDispatch(dispatch, loaders.map((loader) => { - const { id, Renderer, variables, enhancer } = loader - return setWidgetsStart(id, enhancer(), Renderer, variables) + const { id, enhancer } = loader + return setWidgetsStart(id, enhancer()) })) try { diff --git a/src/ui/scripts/components/routes/RouteDomain.js b/src/ui/scripts/components/routes/RouteDomain.js index 4fcf561d..06ea364b 100644 --- a/src/ui/scripts/components/routes/RouteDomain.js +++ b/src/ui/scripts/components/routes/RouteDomain.js @@ -31,6 +31,7 @@ const RouteDomain = (props) => { const factsWidgetConfigs = useMemo(() => [ { + key: 'routeDomainFacts', WidgetComponent: CardFactsWidget, loader: factsLoader(domainId, {}) } @@ -38,6 +39,7 @@ const RouteDomain = (props) => { const essentialWidgetConfigs = useMemo(() => [ { + key: 'routeDomainViews', loader: viewsLoader(domainId, { interval: INTERVALS_DAILY, type: VIEWS_TYPE_UNIQUE, @@ -50,6 +52,7 @@ const RouteDomain = (props) => { } }, { + key: 'routeDomainDurations', loader: durationsLoader(domainId, { interval: INTERVALS_DAILY, limit: 14 @@ -61,6 +64,7 @@ const RouteDomain = (props) => { } }, { + key: 'routeDomainPages', loader: pagesLoader(domainId, { range: RANGES_LAST_24_HOURS, sorting: SORTINGS_TOP @@ -71,6 +75,7 @@ const RouteDomain = (props) => { } }, { + key: 'routeDomainReferrers', loader: referrersLoader(domainId, { range: RANGES_LAST_24_HOURS, sorting: SORTINGS_TOP @@ -84,6 +89,7 @@ const RouteDomain = (props) => { const detailedWidgetConfigs = useMemo(() => [ { + key: 'routeDomainSystems', loader: systemsLoader(domainId, { sorting: SORTINGS_TOP, range: RANGES_LAST_24_HOURS, @@ -95,6 +101,7 @@ const RouteDomain = (props) => { } }, { + key: 'routeDomainDevices', loader: devicesLoader(domainId, { sorting: SORTINGS_TOP, range: RANGES_LAST_24_HOURS, @@ -106,6 +113,7 @@ const RouteDomain = (props) => { } }, { + key: 'routeDomainBrowsers', loader: browsersLoader(domainId, { sorting: SORTINGS_TOP, range: RANGES_LAST_24_HOURS, @@ -117,6 +125,7 @@ const RouteDomain = (props) => { } }, { + key: 'routeDomainSizes', loader: sizesLoader(domainId, { sorting: SORTINGS_TOP, range: RANGES_LAST_24_HOURS, @@ -128,6 +137,7 @@ const RouteDomain = (props) => { } }, { + key: 'routeDomainLanguages', loader: languagesLoader(domainId, { sorting: SORTINGS_TOP, range: RANGES_LAST_24_HOURS diff --git a/src/ui/scripts/hooks/useWidgets.js b/src/ui/scripts/hooks/useWidgets.js index 896bedb6..05525675 100644 --- a/src/ui/scripts/hooks/useWidgets.js +++ b/src/ui/scripts/hooks/useWidgets.js @@ -19,33 +19,53 @@ export default (props, widgetConfigs = []) => { }, [ widgetConfigs ]) - useEffect(() => { - - props.fetchWidgets(props, loaders) - - }, [ loaders ]) + const enhancedWidgetData = useMemo(() => { - return loaders.map( - (loader, index) => { + return loaders.map((loader) => { const widgetId = loader.id - // Ensure that the data is never empty, even when the widget is not ready or - // still loading. Also initialize the value with the correct type using by - // calling the enhancer without parameters. - const widgetData = { + // The loader already contains most data required to render the widget. + // We therefore don't need to wait until the remaining data is available through the store. + return { ...initialSubState(), + // Initialize the value with the correct type by calling the enhancer without parameters. + // This ensures that there's always an empty value with the correct type available to render. value: loader.enhancer(), + Renderer: loader.Renderer, + variables: loader.variables, + // Overwrite the constructed data with the data in the store (when available). ...(props.widgets.value[widgetId] || {}) } + }) + + }, [ loaders, props.widgets.value ]) - const widgetConfig = { + const enhancedWidgetConfigs = useMemo(() => { + + return loaders.map((loader, index) => { + return { ...defaultWidgetConfig, ...widgetConfigs[index] } + }) + + }, [ loaders, widgetConfigs ]) + + useEffect(() => { + + props.fetchWidgets(props, loaders) + + }, [ loaders ]) + + return loaders.map( + (loader, index) => { + const widgetId = loader.id + const widgetState = enhancedWidgetData[index] + const widgetConfig = enhancedWidgetConfigs[index] return h(widgetConfig.WidgetComponent, { key: widgetConfig.key || widgetId, - widget: widgetData, + widget: widgetState, ...widgetConfig.additionalProps }) } diff --git a/src/ui/scripts/reducers/widgets.js b/src/ui/scripts/reducers/widgets.js index ac326a08..8f4f0cc8 100644 --- a/src/ui/scripts/reducers/widgets.js +++ b/src/ui/scripts/reducers/widgets.js @@ -13,8 +13,6 @@ export const initialState = () => ({ export const initialSubState = () => ({ value: undefined, - Renderer: () => null, - variables: {}, fetching: false, error: undefined }) @@ -29,8 +27,6 @@ export default produce((draft, action) => { const value = draft.value[action.id].value == null ? action.value : draft.value[action.id].value draft.value[action.id].value = value // Set remaining data - draft.value[action.id].Renderer = action.Renderer - draft.value[action.id].variables = action.variables draft.value[action.id].fetching = true break case SET_WIDGETS_END: From 0189faf485f8e09154079a0d8b9983c412b1ff83 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 14:17:33 +0100 Subject: [PATCH 093/208] Refactor useWidgets --- src/ui/scripts/hooks/useWidgets.js | 72 ++++++++++++------------------ 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/src/ui/scripts/hooks/useWidgets.js b/src/ui/scripts/hooks/useWidgets.js index 05525675..4ea60551 100644 --- a/src/ui/scripts/hooks/useWidgets.js +++ b/src/ui/scripts/hooks/useWidgets.js @@ -4,67 +4,51 @@ import { initialSubState } from '../reducers/widgets' import CardWidget from '../components/cards/CardWidget' -const defaultWidgetConfig = { - WidgetComponent: CardWidget, - additionalProps: {} -} - export default (props, widgetConfigs = []) => { - const loaders = useMemo(() => { + const enhancedWidgetConfigs = useMemo(() => { - return widgetConfigs.map((widgetConfig) => - widgetConfig.loader - ) + // Set defaults and overwrite with the config passed to the hook + return widgetConfigs.map((widgetConfig) => ({ + key: widgetConfig.loader.id, + WidgetComponent: CardWidget, + additionalProps: {}, + ...widgetConfig + })) }, [ widgetConfigs ]) const enhancedWidgetData = useMemo(() => { - return loaders.map((loader) => { - const widgetId = loader.id - - // The loader already contains most data required to render the widget. - // We therefore don't need to wait until the remaining data is available through the store. - return { - ...initialSubState(), - // Initialize the value with the correct type by calling the enhancer without parameters. - // This ensures that there's always an empty value with the correct type available to render. - value: loader.enhancer(), - Renderer: loader.Renderer, - variables: loader.variables, - // Overwrite the constructed data with the data in the store (when available). - ...(props.widgets.value[widgetId] || {}) - } - }) - - }, [ loaders, props.widgets.value ]) - - const enhancedWidgetConfigs = useMemo(() => { - - return loaders.map((loader, index) => { - return { - ...defaultWidgetConfig, - ...widgetConfigs[index] - } - }) - - }, [ loaders, widgetConfigs ]) + // The loader already contains most data required to render the widget. + // We therefore don't need to wait until the remaining data is available through the store. + return enhancedWidgetConfigs.map(({ loader }) => ({ + ...initialSubState(), + // Initialize the value with the correct type by calling the enhancer without parameters. + // This ensures that there's always an empty value with the correct type available to render. + value: loader.enhancer(), + Renderer: loader.Renderer, + variables: loader.variables, + // Overwrite the constructed data with the data in the store (when available). + ...(props.widgets.value[loader.id] || {}) + })) + + }, [ enhancedWidgetConfigs, props.widgets.value ]) useEffect(() => { + // Fetch widgets in useEffect, because updating the state inside useMemo isn't allowed + const loaders = widgetConfigs.map(({ loader }) => loader) props.fetchWidgets(props, loaders) - }, [ loaders ]) + }, [ enhancedWidgetConfigs ]) - return loaders.map( - (loader, index) => { - const widgetId = loader.id + return enhancedWidgetConfigs.map( + (widgetConfig, index) => { const widgetState = enhancedWidgetData[index] - const widgetConfig = enhancedWidgetConfigs[index] return h(widgetConfig.WidgetComponent, { - key: widgetConfig.key || widgetId, + key: widgetConfig.key, widget: widgetState, ...widgetConfig.additionalProps }) From 8a3fba2da67a4beadcf463d7878467a53fcf204a Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 16:39:44 +0100 Subject: [PATCH 094/208] Fix empty state --- src/ui/scripts/components/cards/CardWidget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/scripts/components/cards/CardWidget.js b/src/ui/scripts/components/cards/CardWidget.js index 6f4cdbd2..daeaca5c 100644 --- a/src/ui/scripts/components/cards/CardWidget.js +++ b/src/ui/scripts/components/cards/CardWidget.js @@ -27,7 +27,7 @@ const CardWidget = (props) => { } if (isEmpty === true) { - h(PresentationEmptyState, { + return h(PresentationEmptyState, { icon: ICON_WARNING }, 'No data') } From ea2519fa9325ed6a535dae2478e3c103bde79653 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 16:40:53 +0100 Subject: [PATCH 095/208] Show events in RouteEvents --- src/ui/scripts/components/Dashboard.js | 1 + .../scripts/components/routes/RouteEvents.js | 10 ++++- .../components/routes/RouteSettings.js | 3 +- .../enhancers/enhanceEventChartEntries.js | 9 ++++ .../enhancers/enhanceEventListEntries.js | 9 ++++ src/ui/scripts/hooks/useWidgetsForEvents.js | 40 ++++++++++++++++++ .../loaders/eventChartEntriesLoader.js | 42 +++++++++++++++++++ .../scripts/loaders/eventListEntriesLoader.js | 40 ++++++++++++++++++ src/ui/scripts/utils/createWidgetId.js | 4 +- 9 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 src/ui/scripts/enhancers/enhanceEventChartEntries.js create mode 100644 src/ui/scripts/enhancers/enhanceEventListEntries.js create mode 100644 src/ui/scripts/hooks/useWidgetsForEvents.js create mode 100644 src/ui/scripts/loaders/eventChartEntriesLoader.js create mode 100644 src/ui/scripts/loaders/eventListEntriesLoader.js diff --git a/src/ui/scripts/components/Dashboard.js b/src/ui/scripts/components/Dashboard.js index 43fc8890..809facfc 100644 --- a/src/ui/scripts/components/Dashboard.js +++ b/src/ui/scripts/components/Dashboard.js @@ -25,6 +25,7 @@ const Dashboard = (props) => { useEffect(() => { props.fetchDomains(props) + props.fetchEvents(props) }, []) diff --git a/src/ui/scripts/components/routes/RouteEvents.js b/src/ui/scripts/components/routes/RouteEvents.js index d4d9d0da..fdc24e7e 100644 --- a/src/ui/scripts/components/routes/RouteEvents.js +++ b/src/ui/scripts/components/routes/RouteEvents.js @@ -1,8 +1,14 @@ import { createElement as h } from 'react' -const RouteEvents = () => { +import useWidgetsForEvents from '../../hooks/useWidgetsForEvents' - return null +const RouteEvents = (props) => { + + return useWidgetsForEvents(props, { + interval: props.filter.interval, + sorting: props.filter.sorting, + range: props.filter.range + }) } diff --git a/src/ui/scripts/components/routes/RouteSettings.js b/src/ui/scripts/components/routes/RouteSettings.js index 5b2f9f58..9389d3ee 100644 --- a/src/ui/scripts/components/routes/RouteSettings.js +++ b/src/ui/scripts/components/routes/RouteSettings.js @@ -25,8 +25,9 @@ const RouteSettings = (props) => { useEffect(() => { - props.fetchPermanentTokens(props) + props.fetchDomains(props) props.fetchEvents(props) + props.fetchPermanentTokens(props) }, []) diff --git a/src/ui/scripts/enhancers/enhanceEventChartEntries.js b/src/ui/scripts/enhancers/enhanceEventChartEntries.js new file mode 100644 index 00000000..bcb266f4 --- /dev/null +++ b/src/ui/scripts/enhancers/enhanceEventChartEntries.js @@ -0,0 +1,9 @@ +import createArray from '../../../utils/createArray' + +export default (chartEntries = [], length) => createArray(length).map((_, index) => { + + const chartEntry = chartEntries[index] + + return chartEntry == null ? 0 : chartEntry.count + +}) \ No newline at end of file diff --git a/src/ui/scripts/enhancers/enhanceEventListEntries.js b/src/ui/scripts/enhancers/enhanceEventListEntries.js new file mode 100644 index 00000000..41bda280 --- /dev/null +++ b/src/ui/scripts/enhancers/enhanceEventListEntries.js @@ -0,0 +1,9 @@ +export default (listEntries = []) => { + + return listEntries.map((listEntry) => ({ + text: listEntry.id, + count: listEntry.count, + date: listEntry.created == null ? null : new Date(listEntry.created) + })) + +} \ No newline at end of file diff --git a/src/ui/scripts/hooks/useWidgetsForEvents.js b/src/ui/scripts/hooks/useWidgetsForEvents.js new file mode 100644 index 00000000..49e8c9e3 --- /dev/null +++ b/src/ui/scripts/hooks/useWidgetsForEvents.js @@ -0,0 +1,40 @@ +import { createElement as h, useMemo } from 'react' + +import { EVENTS_TYPE_CHART, EVENTS_TYPE_LIST } from '../../../constants/events' + +import eventChartEntriesLoader from '../loaders/eventChartEntriesLoader' +import eventListEntriesLoader from '../loaders/eventListEntriesLoader' + +import useWidgets from './useWidgets' + +const selectLoader = (eventId, eventType, opts) => { + switch (eventType) { + case EVENTS_TYPE_CHART: + return eventChartEntriesLoader(eventId, { + interval: opts.interval + }) + case EVENTS_TYPE_LIST: + return eventListEntriesLoader(eventId, { + sorting: opts.sorting, + range: opts.range + }) + } +} + +export default (props, opts) => { + + const widgetConfigs = useMemo(() => { + + return props.events.value.map((event) => ({ + key: event.id, + loader: selectLoader(event.id, event.type, opts), + additionalProps: { + headline: event.title + } + })) + + }, [ props.events.value, ...Object.values(opts) ]) + + return useWidgets(props, widgetConfigs) + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/eventChartEntriesLoader.js b/src/ui/scripts/loaders/eventChartEntriesLoader.js new file mode 100644 index 00000000..b4e25088 --- /dev/null +++ b/src/ui/scripts/loaders/eventChartEntriesLoader.js @@ -0,0 +1,42 @@ +import { createElement as h } from 'react' + +import RendererChart from '../components/renderers/RendererChart' +import enhanceEventChartEntries from '../enhancers/enhanceEventChartEntries' +import formatNumber from '../utils/formatNumber' +import createWidgetId from '../utils/createWidgetId' + +export default (eventId, opts) => { + + const id = createWidgetId('fetchEventsChart', eventId, opts) + + const query = ` + event(id: "${ eventId }") { + statistics { + chart(interval: ${ opts.interval }, limit: 7) { + id + count + } + } + } + ` + + const variables = { + eventId, + interval: opts.interval + } + + const selector = (data, entryName = 'event') => data[entryName].statistics.chart + + return { + id, + Renderer: (props) => h(RendererChart, { + ...props, + formatter: formatNumber + }), + query, + variables, + selector, + enhancer: (chartEntries) => enhanceEventChartEntries(chartEntries, 7) + } + +} \ No newline at end of file diff --git a/src/ui/scripts/loaders/eventListEntriesLoader.js b/src/ui/scripts/loaders/eventListEntriesLoader.js new file mode 100644 index 00000000..c5e914f3 --- /dev/null +++ b/src/ui/scripts/loaders/eventListEntriesLoader.js @@ -0,0 +1,40 @@ +import { createElement as h } from 'react' + +import RendererList from '../components/renderers/RendererList' +import enhanceEventListEntries from '../enhancers/enhanceEventListEntries' +import createWidgetId from '../utils/createWidgetId' + +export default (eventId, opts) => { + + const id = createWidgetId('fetchEventList', eventId, opts) + + const query = ` + event(id: "${ eventId }") { + statistics { + list(sorting: ${ opts.sorting }, range: ${ opts.range }) { + id + count + created + } + } + } + ` + + const variables = { + eventId, + sorting: opts.sorting, + range: opts.range + } + + const selector = (data, entryName = 'event') => data[entryName].statistics.list + + return { + id, + Renderer: RendererList, + query, + variables, + selector, + enhancer: enhanceEventListEntries + } + +} \ No newline at end of file diff --git a/src/ui/scripts/utils/createWidgetId.js b/src/ui/scripts/utils/createWidgetId.js index c269c199..5409cf41 100644 --- a/src/ui/scripts/utils/createWidgetId.js +++ b/src/ui/scripts/utils/createWidgetId.js @@ -1,8 +1,8 @@ import { v5 as uuid } from 'uuid' -export default (type, domainId, opts) => { +export default (type, id, opts) => { - const name = `${ type }${ domainId || '' }${ JSON.stringify(opts) }` + const name = `${ type }${ id || '' }${ JSON.stringify(opts) }` const namespace = '906d7dd0-b6e0-42c8-9270-436958c29c36' return uuid(name, namespace) From 0e184fff8ef30a5170f6e66aac14f3ae5e5ad749 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 17:36:34 +0100 Subject: [PATCH 096/208] Fix permanent token list --- src/ui/scripts/components/routes/RouteSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/scripts/components/routes/RouteSettings.js b/src/ui/scripts/components/routes/RouteSettings.js index 9389d3ee..a9d20427 100644 --- a/src/ui/scripts/components/routes/RouteSettings.js +++ b/src/ui/scripts/components/routes/RouteSettings.js @@ -67,7 +67,7 @@ const RouteSettings = (props) => { const domainsItems = createItems(props.domains.value, showDomainEditModal, showDomainAddModal, 'New domain') const eventsItems = createItems(props.events.value, showEventEditModal, showEventAddModal, 'New event') - const permanentTokensItems = createItems(props.domains.value, showPermanentTokenEditModal, showPermanentTokenAddModal, 'New permanent token') + const permanentTokensItems = createItems(props.permanentTokens.value, showPermanentTokenEditModal, showPermanentTokenAddModal, 'New permanent token') return ( h(Fragment, {}, From 43c7a1d056bedf4678e0b94f779254015f1e1d7f Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 13 Dec 2020 18:16:28 +0100 Subject: [PATCH 097/208] Loading and updating status --- src/ui/scripts/components/Loader.js | 13 +++++++ src/ui/scripts/components/Status.js | 26 +++++++++++++ src/ui/scripts/components/Updating.js | 16 -------- src/ui/scripts/components/cards/CardWidget.js | 39 +++++++++---------- .../scripts/components/icons/IconLoading.js | 23 ----------- .../scripts/components/icons/IconWarning.js | 23 ----------- .../presentations/PresentationEmptyState.js | 28 ------------- .../components/renderers/RendererChart.js | 4 +- .../components/renderers/RendererList.js | 4 +- .../components/renderers/RendererReferrers.js | 4 +- src/ui/styles/_loader.scss | 36 +++++++++++++++++ src/ui/styles/_spinner.scss | 2 +- .../styles/{_updating.scss => _status.scss} | 2 +- src/ui/styles/index.scss | 3 +- 14 files changed, 104 insertions(+), 119 deletions(-) create mode 100644 src/ui/scripts/components/Loader.js create mode 100644 src/ui/scripts/components/Status.js delete mode 100644 src/ui/scripts/components/Updating.js delete mode 100644 src/ui/scripts/components/icons/IconLoading.js delete mode 100644 src/ui/scripts/components/icons/IconWarning.js delete mode 100644 src/ui/scripts/components/presentations/PresentationEmptyState.js create mode 100644 src/ui/styles/_loader.scss rename src/ui/styles/{_updating.scss => _status.scss} (90%) diff --git a/src/ui/scripts/components/Loader.js b/src/ui/scripts/components/Loader.js new file mode 100644 index 00000000..e4b2cadf --- /dev/null +++ b/src/ui/scripts/components/Loader.js @@ -0,0 +1,13 @@ +import { createElement as h } from 'react' + +const Loader = () => { + + return ( + h('div', { className: 'loader' }, + h('div', { className: 'loader__circle' }) + ) + ) + +} + +export default Loader \ No newline at end of file diff --git a/src/ui/scripts/components/Status.js b/src/ui/scripts/components/Status.js new file mode 100644 index 00000000..55b4e222 --- /dev/null +++ b/src/ui/scripts/components/Status.js @@ -0,0 +1,26 @@ +import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +import Loader from './Loader' +import Updater from './Updater' + +export const ICON_LOADER = Loader +export const ICON_UPDATER = Updater + +const Status = (props) => { + + return ( + h('div', { className: 'status' }, + props.icon && h(props.icon, {}), + props.children + ) + ) + +} + +Status.propTypes = { + icon: PropTypes.oneOf([ ICON_LOADER, ICON_UPDATER ]), + children: PropTypes.node.isRequired +} + +export default Status \ No newline at end of file diff --git a/src/ui/scripts/components/Updating.js b/src/ui/scripts/components/Updating.js deleted file mode 100644 index fab0796b..00000000 --- a/src/ui/scripts/components/Updating.js +++ /dev/null @@ -1,16 +0,0 @@ -import { createElement as h } from 'react' - -import Updater from './Updater' - -const Updating = () => { - - return ( - h('div', { className: 'updating' }, - h(Updater, {}), - 'Updating' - ) - ) - -} - -export default Updating \ No newline at end of file diff --git a/src/ui/scripts/components/cards/CardWidget.js b/src/ui/scripts/components/cards/CardWidget.js index daeaca5c..3c206a42 100644 --- a/src/ui/scripts/components/cards/CardWidget.js +++ b/src/ui/scripts/components/cards/CardWidget.js @@ -4,8 +4,7 @@ import classNames from 'classnames' import Headline from '../Headline' import Text from '../Text' -import Updating from '../Updating' -import PresentationEmptyState, { ICON_LOADING, ICON_WARNING } from '../presentations/PresentationEmptyState' +import Status, { ICON_LOADER, ICON_UPDATER } from '../Status' import status from '../../utils/status' const CardWidget = (props) => { @@ -17,25 +16,22 @@ const CardWidget = (props) => { } = status(props.widget.value, props.widget.fetching) // Use thin space as initial value to avoid that the label changes the height once rendered - const [ textLabel, setTextLabel ] = useState(' ') + const [ statusLabel, setStatusLabel ] = useState(' ') - const presentation = (() => { - if (isLoading === true) { - return h(PresentationEmptyState, { - icon: ICON_LOADING - }, 'Loading data') - } + const currentStatus = (() => { + if (isLoading === true) return h(Status, { + icon: ICON_LOADER + }, 'Loading') - if (isEmpty === true) { - return h(PresentationEmptyState, { - icon: ICON_WARNING - }, 'No data') - } + if (isStale === true) return h(Status, { + icon: ICON_UPDATER + }, 'Updating') - return h(props.widget.Renderer, { - widget: props.widget, - setTextLabel - }) + if (isEmpty === true) return h(Status, {}, + 'No data' + ) + + return h(Status, {}, statusLabel) })() return ( @@ -54,8 +50,11 @@ const CardWidget = (props) => { h(Text, { type: 'div', spacing: false - }, isStale === true ? h(Updating) : textLabel), - presentation + }, currentStatus), + h(props.widget.Renderer, { + widget: props.widget, + setStatusLabel + }) ) ) ) diff --git a/src/ui/scripts/components/icons/IconLoading.js b/src/ui/scripts/components/icons/IconLoading.js deleted file mode 100644 index d62a4862..00000000 --- a/src/ui/scripts/components/icons/IconLoading.js +++ /dev/null @@ -1,23 +0,0 @@ -import { createElement as h } from 'react' -import PropTypes from 'prop-types' - -const IconLoading = (props) => { - - return ( - h('svg', { - viewBox: '0 0 24 24', - className: props.className - }, - h('path', { - d: 'M18.513 7.119c.958-1.143 1.487-2.577 1.487-4.036v-3.083h-16v3.083c0 1.459.528 2.892 1.487 4.035l3.086 3.68c.567.677.571 1.625.009 2.306l-3.13 3.794c-.936 1.136-1.452 2.555-1.452 3.995v3.107h16v-3.107c0-1.44-.517-2.858-1.453-3.994l-3.13-3.794c-.562-.681-.558-1.629.009-2.306l3.087-3.68zm-4.639 7.257l3.13 3.794c.652.792.996 1.726.996 2.83h-12c0-1.104.343-2.039.996-2.829l3.129-3.793c1.167-1.414 1.159-3.459-.019-4.864l-3.086-3.681c-.66-.785-1.02-1.736-1.02-2.834h12c0 1.101-.363 2.05-1.02 2.834l-3.087 3.68c-1.177 1.405-1.185 3.451-.019 4.863z' - }) - ) - ) - -} - -IconLoading.propTypes = { - className: PropTypes.string -} - -export default IconLoading \ No newline at end of file diff --git a/src/ui/scripts/components/icons/IconWarning.js b/src/ui/scripts/components/icons/IconWarning.js deleted file mode 100644 index ffe7ee71..00000000 --- a/src/ui/scripts/components/icons/IconWarning.js +++ /dev/null @@ -1,23 +0,0 @@ -import { createElement as h } from 'react' -import PropTypes from 'prop-types' - -const IconWarning = (props) => { - - return ( - h('svg', { - viewBox: '0 0 24 24', - className: props.className - }, - h('path', { - d: 'M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-1.5 6h3l-1 8h-1l-1-8zm1.5 12.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z' - }) - ) - ) - -} - -IconWarning.propTypes = { - className: PropTypes.string -} - -export default IconWarning \ No newline at end of file diff --git a/src/ui/scripts/components/presentations/PresentationEmptyState.js b/src/ui/scripts/components/presentations/PresentationEmptyState.js deleted file mode 100644 index 4adc771a..00000000 --- a/src/ui/scripts/components/presentations/PresentationEmptyState.js +++ /dev/null @@ -1,28 +0,0 @@ -import { createElement as h } from 'react' -import PropTypes from 'prop-types' - -import IconLoading from '../icons/IconLoading' -import IconWarning from '../icons/IconWarning' - -export const ICON_LOADING = IconLoading -export const ICON_WARNING = IconWarning - -const PresentationEmptyState = (props) => { - - return ( - h('div', { className: 'emptyState' }, - h('div', { className: 'emptyState__inner' }, - h(props.icon, { className: 'emptyState__icon' }), - props.children - ) - ) - ) - -} - -PresentationEmptyState.propTypes = { - icon: PropTypes.oneOf([ ICON_LOADING, ICON_WARNING ]).isRequired, - children: PropTypes.node.isRequired -} - -export default PresentationEmptyState \ No newline at end of file diff --git a/src/ui/scripts/components/renderers/RendererChart.js b/src/ui/scripts/components/renderers/RendererChart.js index 670e3007..dc5f7102 100644 --- a/src/ui/scripts/components/renderers/RendererChart.js +++ b/src/ui/scripts/components/renderers/RendererChart.js @@ -36,7 +36,7 @@ const RendererChart = (props) => { const onLeave = () => setActive(0) const label = textLabel(active, interval) - useEffect(() => props.setTextLabel(label), [ label ]) + useEffect(() => props.setStatusLabel(label), [ label ]) return h(PresentationBarChart, { items, @@ -50,7 +50,7 @@ const RendererChart = (props) => { RendererChart.propTypes = { widget: PropTypes.object.isRequired, - setTextLabel: PropTypes.func.isRequired + setStatusLabel: PropTypes.func.isRequired } export default RendererChart \ No newline at end of file diff --git a/src/ui/scripts/components/renderers/RendererList.js b/src/ui/scripts/components/renderers/RendererList.js index 5a13d73a..69d67c23 100644 --- a/src/ui/scripts/components/renderers/RendererList.js +++ b/src/ui/scripts/components/renderers/RendererList.js @@ -29,7 +29,7 @@ const RendererList = (props) => { const onLeave = () => setActive() const label = textLabel(items[active], range, sorting === SORTINGS_RECENT) - useEffect(() => props.setTextLabel(label), [ label ]) + useEffect(() => props.setStatusLabel(label), [ label ]) if (sorting === SORTINGS_TOP) return h(PresentationCounterList, { items @@ -45,7 +45,7 @@ const RendererList = (props) => { RendererList.propTypes = { widget: PropTypes.object.isRequired, - setTextLabel: PropTypes.func.isRequired + setStatusLabel: PropTypes.func.isRequired } export default RendererList \ No newline at end of file diff --git a/src/ui/scripts/components/renderers/RendererReferrers.js b/src/ui/scripts/components/renderers/RendererReferrers.js index 2dc3fe53..261b4878 100644 --- a/src/ui/scripts/components/renderers/RendererReferrers.js +++ b/src/ui/scripts/components/renderers/RendererReferrers.js @@ -31,7 +31,7 @@ const RendererReferrers = (props) => { const onLeave = () => setActive() const label = textLabel(items[active], range, sorting === SORTINGS_RECENT, sorting === SORTINGS_NEW) - useEffect(() => props.setTextLabel(label), [ label ]) + useEffect(() => props.setStatusLabel(label), [ label ]) return h(PresentationIconList, { items, @@ -43,7 +43,7 @@ const RendererReferrers = (props) => { RendererReferrers.propTypes = { widget: PropTypes.object.isRequired, - setTextLabel: PropTypes.func.isRequired + setStatusLabel: PropTypes.func.isRequired } export default RendererReferrers \ No newline at end of file diff --git a/src/ui/styles/_loader.scss b/src/ui/styles/_loader.scss new file mode 100644 index 00000000..d8e84a0a --- /dev/null +++ b/src/ui/styles/_loader.scss @@ -0,0 +1,36 @@ +.loader { + + $size: 1em; + + @keyframes loader__rotate { + 0% { + transform: rotate(0turn); + } + 100% { + transform: rotate(1turn); + } + } + + position: relative; + width: $size; + height: $size; + border-radius: 100%; + background: $context; + + &__circle { + position: absolute; + width: 100%; + height: 100%; + border-style: solid; + border-width: 2px; + border-color: transparent; + border-radius: inherit; + border-right-color: $white; + animation-name: loader__rotate; + animation-iteration-count: infinite; + animation-timing-function: linear; + animation-duration: .5s; + opacity: .6; + } + +} \ No newline at end of file diff --git a/src/ui/styles/_spinner.scss b/src/ui/styles/_spinner.scss index cf0f8103..2d438385 100644 --- a/src/ui/styles/_spinner.scss +++ b/src/ui/styles/_spinner.scss @@ -21,8 +21,8 @@ height: 100%; border-style: solid; border-width: 3px; - border-radius: 100%; border-color: transparent; + border-radius: 100%; animation-name: spinner__rotate; animation-iteration-count: infinite; animation-timing-function: linear; diff --git a/src/ui/styles/_updating.scss b/src/ui/styles/_status.scss similarity index 90% rename from src/ui/styles/_updating.scss rename to src/ui/styles/_status.scss index c76e65fa..95d1a577 100644 --- a/src/ui/styles/_updating.scss +++ b/src/ui/styles/_status.scss @@ -1,4 +1,4 @@ -.updating { +.status { display: grid; gap: $gutter/2; diff --git a/src/ui/styles/index.scss b/src/ui/styles/index.scss index 934f4686..a026c46b 100644 --- a/src/ui/styles/index.scss +++ b/src/ui/styles/index.scss @@ -21,6 +21,7 @@ @import 'card'; @import 'spinner'; @import 'updater'; +@import 'loader'; @import 'message'; @import 'header'; @import 'content'; @@ -35,7 +36,7 @@ @import 'context'; @import 'filter'; @import 'keyHint'; -@import 'updating'; +@import 'status'; @import 'facts'; // Helpers ----------------------------------------------------- // From 1d47c8024e3946355e932f2481af065f9a5b3584 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 19 Dec 2020 11:32:08 +0100 Subject: [PATCH 098/208] New UI radius --- src/ui/styles/_context.scss | 2 +- src/ui/styles/_message.scss | 2 +- src/ui/styles/_vars.scss | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ui/styles/_context.scss b/src/ui/styles/_context.scss index fb21aa0f..5f5cefd9 100644 --- a/src/ui/styles/_context.scss +++ b/src/ui/styles/_context.scss @@ -55,7 +55,7 @@ margin: 0 $gutter/2; padding: $gutter/2 $gutter/1.6; color: $white; - border-radius: $radius; + border-radius: $innerRadius; &.active { color: $primary; diff --git a/src/ui/styles/_message.scss b/src/ui/styles/_message.scss index 5a4cb47f..f39d7c44 100644 --- a/src/ui/styles/_message.scss +++ b/src/ui/styles/_message.scss @@ -4,7 +4,7 @@ // Make the padding and border look like formbase padding: $formbase__padding; border: 2px solid transparent; - border-radius: $radius; + border-radius: $innerRadius; color: $white; font-size: .8em; diff --git a/src/ui/styles/_vars.scss b/src/ui/styles/_vars.scss index 1972ad6e..95f1375f 100644 --- a/src/ui/styles/_vars.scss +++ b/src/ui/styles/_vars.scss @@ -18,7 +18,8 @@ $destructive: rgba(255, 60, 60, 1); // Sizes ------------------------------------------------------- // $gutter: 1rem; $maxWidth: 1200px; -$radius: 6px; +$radius: 10px; +$innerRadius: 8px; $cardContentHeight: 300px; $rowHeight: 32px; @@ -26,7 +27,7 @@ $rowHeight: 32px; $shadow: 0 2px 10px rgba(0, 0, 0, .12); // formbase ---------------------------------------------------- // -$formbase__radius: $radius; +$formbase__radius: $innerRadius; $formbase__background: $dimmed; $formbase__svg: #fff; $formbase__color: $white; From b48d32a007da041fc7fe01bc1ef161c4911d2fee Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 19 Dec 2020 12:06:47 +0100 Subject: [PATCH 099/208] Improved error handling for widgets --- src/ui/scripts/actions/widgets.js | 5 ++--- src/ui/scripts/enhancers/enhanceState.js | 2 +- src/ui/scripts/reducers/widgets.js | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ui/scripts/actions/widgets.js b/src/ui/scripts/actions/widgets.js index e024c4b0..aa5dd5e5 100644 --- a/src/ui/scripts/actions/widgets.js +++ b/src/ui/scripts/actions/widgets.js @@ -25,9 +25,8 @@ export const setWidgetsFetching = (id, payload) => ({ payload }) -export const setWidgetsError = (id, payload) => ({ +export const setWidgetsError = (payload) => ({ type: SET_WIDGETS_ERROR, - id, payload }) @@ -75,7 +74,7 @@ export const fetchWidgets = signalHandler((signal) => (props, loaders) => async if (err.name === 'AbortError') return batchDispatch(dispatch, loaders.map((loader) => setWidgetsFetching(loader.id, false))) if (err.name === 'HandledError') return - batchDispatch(dispatch, loaders.map((loader) => setWidgetsError(loader.id, err))) + dispatch(setWidgetsError(err)) } diff --git a/src/ui/scripts/enhancers/enhanceState.js b/src/ui/scripts/enhancers/enhanceState.js index ba03c9f5..b04309a5 100644 --- a/src/ui/scripts/enhancers/enhanceState.js +++ b/src/ui/scripts/enhancers/enhanceState.js @@ -11,7 +11,7 @@ export default (state) => { ) const errors = [ - ...Object.values(state.widgets.value).map((value) => value.error), + state.widgets.error, state.domains.error, state.token.error, state.permanentTokens.error, diff --git a/src/ui/scripts/reducers/widgets.js b/src/ui/scripts/reducers/widgets.js index 8f4f0cc8..29a5ea23 100644 --- a/src/ui/scripts/reducers/widgets.js +++ b/src/ui/scripts/reducers/widgets.js @@ -8,13 +8,13 @@ import { } from '../actions' export const initialState = () => ({ - value: {} + value: {}, + error: undefined }) export const initialSubState = () => ({ value: undefined, - fetching: false, - error: undefined + fetching: false }) export default produce((draft, action) => { @@ -37,7 +37,7 @@ export default produce((draft, action) => { draft.value[action.id].fetching = action.payload || initialSubState().fetching break case SET_WIDGETS_ERROR: - draft.value[action.id].error = action.payload || initialSubState().error + draft.error = action.payload || initialState().error break } From 1f06785bddff9b368857d6bbf573e450c75ba1c9 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 19 Dec 2020 12:08:15 +0100 Subject: [PATCH 100/208] Smoothly animated bar charts --- src/ui/scripts/loaders/durationsLoader.js | 10 ++++++---- src/ui/scripts/loaders/eventChartEntriesLoader.js | 10 ++++++---- src/ui/scripts/loaders/mergedDurationsLoader.js | 8 ++------ src/ui/scripts/loaders/mergedViewsLoader.js | 8 ++------ src/ui/scripts/loaders/viewsLoader.js | 10 ++++++---- 5 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/ui/scripts/loaders/durationsLoader.js b/src/ui/scripts/loaders/durationsLoader.js index 29252add..72f0cd81 100644 --- a/src/ui/scripts/loaders/durationsLoader.js +++ b/src/ui/scripts/loaders/durationsLoader.js @@ -5,6 +5,11 @@ import enhanceDurations from '../enhancers/enhanceDurations' import formatDuration from '../utils/formatDuration' import createWidgetId from '../utils/createWidgetId' +export const DurationsChartRenderer = (props) => h(RendererChart, { + ...props, + formatter: (ms) => formatDuration(ms).toString() +}) + export default (domainId, opts) => { const id = createWidgetId('fetchDurations', domainId, opts) @@ -30,10 +35,7 @@ export default (domainId, opts) => { return { id, - Renderer: (props) => h(RendererChart, { - ...props, - formatter: (ms) => formatDuration(ms).toString() - }), + Renderer: DurationsChartRenderer, query, variables, selector, diff --git a/src/ui/scripts/loaders/eventChartEntriesLoader.js b/src/ui/scripts/loaders/eventChartEntriesLoader.js index b4e25088..b4739920 100644 --- a/src/ui/scripts/loaders/eventChartEntriesLoader.js +++ b/src/ui/scripts/loaders/eventChartEntriesLoader.js @@ -5,6 +5,11 @@ import enhanceEventChartEntries from '../enhancers/enhanceEventChartEntries' import formatNumber from '../utils/formatNumber' import createWidgetId from '../utils/createWidgetId' +const EventEntriesChartRenderer = (props) => h(RendererChart, { + ...props, + formatter: formatNumber +}) + export default (eventId, opts) => { const id = createWidgetId('fetchEventsChart', eventId, opts) @@ -29,10 +34,7 @@ export default (eventId, opts) => { return { id, - Renderer: (props) => h(RendererChart, { - ...props, - formatter: formatNumber - }), + Renderer: EventEntriesChartRenderer, query, variables, selector, diff --git a/src/ui/scripts/loaders/mergedDurationsLoader.js b/src/ui/scripts/loaders/mergedDurationsLoader.js index 46c621bb..e7183237 100644 --- a/src/ui/scripts/loaders/mergedDurationsLoader.js +++ b/src/ui/scripts/loaders/mergedDurationsLoader.js @@ -1,8 +1,7 @@ import { createElement as h } from 'react' -import RendererChart from '../components/renderers/RendererChart' +import { DurationsChartRenderer } from './durationsLoader' import enhanceDurations from '../enhancers/enhanceDurations' -import formatDuration from '../utils/formatDuration' import createWidgetId from '../utils/createWidgetId' export default (opts) => { @@ -27,10 +26,7 @@ export default (opts) => { return { id, - Renderer: (props) => h(RendererChart, { - ...props, - formatter: (ms) => formatDuration(ms).toString() - }), + Renderer: DurationsChartRenderer, query, variables, selector, diff --git a/src/ui/scripts/loaders/mergedViewsLoader.js b/src/ui/scripts/loaders/mergedViewsLoader.js index 6504945a..30d9d439 100644 --- a/src/ui/scripts/loaders/mergedViewsLoader.js +++ b/src/ui/scripts/loaders/mergedViewsLoader.js @@ -1,8 +1,7 @@ import { createElement as h } from 'react' -import RendererChart from '../components/renderers/RendererChart' +import { ViewsChartRenderer } from './viewsLoader' import enhanceViews from '../enhancers/enhanceViews' -import formatNumber from '../utils/formatNumber' import createWidgetId from '../utils/createWidgetId' export default (opts) => { @@ -28,10 +27,7 @@ export default (opts) => { return { id, - Renderer: (props) => h(RendererChart, { - ...props, - formatter: formatNumber - }), + Renderer: ViewsChartRenderer, query, variables, selector, diff --git a/src/ui/scripts/loaders/viewsLoader.js b/src/ui/scripts/loaders/viewsLoader.js index d44b207b..cc12e203 100644 --- a/src/ui/scripts/loaders/viewsLoader.js +++ b/src/ui/scripts/loaders/viewsLoader.js @@ -5,6 +5,11 @@ import enhanceViews from '../enhancers/enhanceViews' import formatNumber from '../utils/formatNumber' import createWidgetId from '../utils/createWidgetId' +export const ViewsChartRenderer = (props) => h(RendererChart, { + ...props, + formatter: formatNumber +}) + export default (domainId, opts) => { const id = createWidgetId('fetchViews', domainId, opts) @@ -31,10 +36,7 @@ export default (domainId, opts) => { return { id, - Renderer: (props) => h(RendererChart, { - ...props, - formatter: formatNumber - }), + Renderer: ViewsChartRenderer, query, variables, selector, From 5711966e19881c4fecd8b791c3003526b5027f08 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 19 Dec 2020 12:29:57 +0100 Subject: [PATCH 101/208] Ensure that action values are positive --- src/types/actions.js | 6 +++--- src/utils/createApolloServer.js | 11 ++++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/types/actions.js b/src/types/actions.js index 9470d736..0e5f057d 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -19,7 +19,7 @@ module.exports = gql` Numerical value that is added to all other numerical values of the key, grouped by day, month or year. Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. """ - value: Float! + value: PositiveFloat! """ Details allow you to store more data along with the associated action. """ @@ -43,7 +43,7 @@ module.exports = gql` Numerical value that is added to all other numerical values of the key, grouped by day, month or year. Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. """ - value: Float! + value: PositiveFloat! """ Details allow you to store more data along with the associated action. """ @@ -70,7 +70,7 @@ module.exports = gql` Numerical value that is added to all other numerical values of the key, grouped by day, month or year. Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. """ - value: Float! + value: PositiveFloat! """ Details allow you to store more data along with the associated action. """ diff --git a/src/utils/createApolloServer.js b/src/utils/createApolloServer.js index 6599ed25..26db1ecd 100644 --- a/src/utils/createApolloServer.js +++ b/src/utils/createApolloServer.js @@ -1,7 +1,14 @@ 'use strict' -const { UnsignedIntResolver, UnsignedIntTypeDefinition, DateTimeResolver, DateTimeTypeDefinition } = require('graphql-scalars') const httpHeadersPlugin = require('apollo-server-plugin-http-headers') +const { + UnsignedIntResolver, + UnsignedIntTypeDefinition, + DateTimeResolver, + DateTimeTypeDefinition, + PositiveFloatResolver, + PositiveFloatTypeDefinition +} = require('graphql-scalars') const isDemoMode = require('./isDemoMode') const isDevelopmentMode = require('./isDevelopmentMode') @@ -16,11 +23,13 @@ module.exports = (ApolloServer, opts) => new ApolloServer({ typeDefs: [ UnsignedIntTypeDefinition, DateTimeTypeDefinition, + PositiveFloatTypeDefinition, require('../types') ], resolvers: { UnsignedInt: UnsignedIntResolver, DateTime: DateTimeResolver, + PositiveFloat: PositiveFloatResolver, ...require('../resolvers') }, ...opts From e5e6d1e16c94ef2f58712b5392052b80682b1491 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 20 Dec 2020 11:23:40 +0100 Subject: [PATCH 102/208] Format count in list --- .../presentations/PresentationCounterList.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ui/scripts/components/presentations/PresentationCounterList.js b/src/ui/scripts/components/presentations/PresentationCounterList.js index ee49856a..a22d2a4e 100644 --- a/src/ui/scripts/components/presentations/PresentationCounterList.js +++ b/src/ui/scripts/components/presentations/PresentationCounterList.js @@ -5,6 +5,12 @@ import enhanceUrl from '../../enhancers/enhanceUrl' import sumByProp from '../../utils/sumByProp' import maxByProp from '../../utils/maxByProp' +const formatCount = (num) => { + + return Number.parseFloat(num).toFixed(2).replace('.00', '') + +} + const Row = (props) => { const hasUrl = props.url != null @@ -38,7 +44,9 @@ const PresentationCounterList = (props) => { const proportionalWidth = ({ count }) => (count / totalCount) * 100 const averageCharWidth = 9 - const counterWidth = (String(props.items.reduce(maxByProp('count'), 0)).length + 1) * averageCharWidth + const maxCount = props.items.reduce(maxByProp('count'), 0) + const formattedCount = formatCount(maxCount) + const counterWidth = (String(formattedCount).length + 1) * averageCharWidth return ( h('div', { className: 'flexList' }, @@ -48,7 +56,9 @@ const PresentationCounterList = (props) => { key: item.text + index, barWidth: proportionalWidth(item), counterWidth, - ...item + count: formatCount(item.count), + url: item.url, + text: item.text }) )) ) From 401c84ce461d5e63ce9ceb0955015542e8de96ce Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 20 Dec 2020 11:24:08 +0100 Subject: [PATCH 103/208] Format event chart as float --- src/ui/scripts/loaders/eventChartEntriesLoader.js | 4 ++-- src/ui/scripts/utils/formatFloat.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 src/ui/scripts/utils/formatFloat.js diff --git a/src/ui/scripts/loaders/eventChartEntriesLoader.js b/src/ui/scripts/loaders/eventChartEntriesLoader.js index b4739920..bc108baf 100644 --- a/src/ui/scripts/loaders/eventChartEntriesLoader.js +++ b/src/ui/scripts/loaders/eventChartEntriesLoader.js @@ -2,12 +2,12 @@ import { createElement as h } from 'react' import RendererChart from '../components/renderers/RendererChart' import enhanceEventChartEntries from '../enhancers/enhanceEventChartEntries' -import formatNumber from '../utils/formatNumber' +import formatFloat from '../utils/formatFloat' import createWidgetId from '../utils/createWidgetId' const EventEntriesChartRenderer = (props) => h(RendererChart, { ...props, - formatter: formatNumber + formatter: formatFloat }) export default (eventId, opts) => { diff --git a/src/ui/scripts/utils/formatFloat.js b/src/ui/scripts/utils/formatFloat.js new file mode 100644 index 00000000..3ce5d437 --- /dev/null +++ b/src/ui/scripts/utils/formatFloat.js @@ -0,0 +1,10 @@ +import humanNumber from 'human-number' + +export default (num) => { + + const formattedNum = humanNumber(num, (num) => Number.parseFloat(num).toFixed(2)) + const cleanNum = formattedNum.replace('.00', '') + + return cleanNum + +} \ No newline at end of file From e116702065e7121433d455da1e51abf1b51d9ed7 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 20 Dec 2020 11:45:08 +0100 Subject: [PATCH 104/208] Adjust spacing --- src/ui/styles/_headline.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/styles/_headline.scss b/src/ui/styles/_headline.scss index 6ba7eee6..afaa72e3 100644 --- a/src/ui/styles/_headline.scss +++ b/src/ui/styles/_headline.scss @@ -8,7 +8,7 @@ line-height: 1; &--medium { - margin: 0 0 $gutter/3; + margin: 0 0 $gutter/4; font-size: 1.5em; line-height: 1.2; } From 1c7fdbd50d3b81571e65cee4dcd8e4ded617420f Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 20 Dec 2020 12:43:43 +0100 Subject: [PATCH 105/208] Copy to clipboard message #166 --- src/ui/scripts/components/Input.js | 48 ++++++++++------- src/ui/scripts/components/Textarea.js | 43 ++++++++++++++-- .../components/modals/ModalDomainEdit.js | 10 +--- .../components/modals/ModalEventEdit.js | 10 +--- .../modals/ModalPermanentTokenEdit.js | 7 +-- .../components/overlays/OverlayFailure.js | 1 - src/ui/styles/_inputMessage.scss | 51 +++++++++++++++++++ src/ui/styles/_vars.scss | 1 + src/ui/styles/index.scss | 1 + 9 files changed, 126 insertions(+), 46 deletions(-) create mode 100644 src/ui/styles/_inputMessage.scss diff --git a/src/ui/scripts/components/Input.js b/src/ui/scripts/components/Input.js index eb2d695e..c4b7df75 100644 --- a/src/ui/scripts/components/Input.js +++ b/src/ui/scripts/components/Input.js @@ -1,6 +1,11 @@ import { createElement as h, useRef, useEffect } from 'react' import PropTypes from 'prop-types' +const copyInput = (e) => { + e.target.select() + document.execCommand('copy') +} + const Input = (props) => { const ref = useRef(null) @@ -21,25 +26,32 @@ const Input = (props) => { password: 'current-password' })[props.type] - return ( - h('input', { - ref, - className: 'input', - autoCapitalize: 'off', - autoCorrect: 'off', - autoComplete, - type, - id: props.id, - required: props.required, - disabled: props.disabled, - readOnly: props.readOnly, - placeholder: props.placeholder, - value: props.value, - onChange: props.onChange, - onFocus: props.onFocus - }) + const input = h('input', { + ref, + className: 'input', + autoCapitalize: 'off', + autoCorrect: 'off', + autoComplete, + type, + id: props.id, + required: props.required, + disabled: props.disabled, + readOnly: props.readOnly, + placeholder: props.placeholder, + value: props.value, + onChange: props.onChange, + onFocus: props.copyOnFocus === true ? copyInput : undefined + }) + + if (props.copyOnFocus === true) return ( + h('div', { + className: 'inputMessage', + title: 'Copied to clipboard' + }, input) ) + return input + } Input.propTypes = { @@ -52,7 +64,7 @@ Input.propTypes = { placeholder: PropTypes.string.isRequired, value: PropTypes.string, onChange: PropTypes.func, - onFocus: PropTypes.func + copyOnFocus: PropTypes.bool } export default Input \ No newline at end of file diff --git a/src/ui/scripts/components/Textarea.js b/src/ui/scripts/components/Textarea.js index 180c4402..f3147026 100644 --- a/src/ui/scripts/components/Textarea.js +++ b/src/ui/scripts/components/Textarea.js @@ -1,14 +1,47 @@ import { createElement as h } from 'react' +import PropTypes from 'prop-types' + +const copyInput = (e) => { + e.target.select() + document.execCommand('copy') +} const Textarea = (props) => { - return ( - h('textarea', { - className: 'input', - ...props - }) + const textarea = h('textarea', { + className: 'input', + id: props.id, + required: props.required, + disabled: props.disabled, + readOnly: props.readOnly, + placeholder: props.placeholder, + value: props.value, + rows: props.rows, + onChange: props.onChange, + onFocus: props.copyOnFocus === true ? copyInput : undefined + }) + + if (props.copyOnFocus === true) return ( + h('div', { + className: 'inputMessage', + title: 'Copied to clipboard' + }, textarea) ) + return textarea + +} + +Textarea.propTypes = { + id: PropTypes.string, + required: PropTypes.bool, + disabled: PropTypes.bool, + readOnly: PropTypes.bool, + placeholder: PropTypes.string, + value: PropTypes.string, + rows: PropTypes.number, + onChange: PropTypes.func, + copyOnFocus: PropTypes.bool } export default Textarea \ No newline at end of file diff --git a/src/ui/scripts/components/modals/ModalDomainEdit.js b/src/ui/scripts/components/modals/ModalDomainEdit.js index d223c254..816bb304 100644 --- a/src/ui/scripts/components/modals/ModalDomainEdit.js +++ b/src/ui/scripts/components/modals/ModalDomainEdit.js @@ -29,11 +29,6 @@ const ModalDomainEdit = (props) => { [key]: e.target.value }) - const copyInput = (e) => { - e.target.select() - document.execCommand('copy') - } - const updateDomain = (e) => { e.preventDefault() props.updateDomain(props.id, inputs).then(props.closeModal) @@ -80,18 +75,17 @@ const ModalDomainEdit = (props) => { readOnly: true, placeholder: 'Domain id', value: props.id, - onFocus: copyInput + copyOnFocus: true }), h(Label, { htmlFor: embedId }, 'Embed code'), h(Textarea, { - type: 'text', id: embedId, readOnly: true, rows: 4, value: ``, - onFocus: copyInput + copyOnFocus: true }) ), diff --git a/src/ui/scripts/components/modals/ModalEventEdit.js b/src/ui/scripts/components/modals/ModalEventEdit.js index 85a234fd..9163e1b6 100644 --- a/src/ui/scripts/components/modals/ModalEventEdit.js +++ b/src/ui/scripts/components/modals/ModalEventEdit.js @@ -32,11 +32,6 @@ const ModalEventEdit = (props) => { [key]: e.target.value }) - const copyInput = (e) => { - e.target.select() - document.execCommand('copy') - } - const updateEvent = (e) => { e.preventDefault() props.updateEvent(props.id, inputs).then(props.closeModal) @@ -100,18 +95,17 @@ const ModalEventEdit = (props) => { readOnly: true, placeholder: 'Event id', value: props.id, - onFocus: copyInput + copyOnFocus: true }), h(Label, { htmlFor: embedId }, 'Embed code'), h(Textarea, { - type: 'text', id: embedId, readOnly: true, rows: 4, value: `ackeeTracker.create`, - onFocus: copyInput + copyOnFocus: true }) ), diff --git a/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js b/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js index 669b57fd..b16c8591 100644 --- a/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js +++ b/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js @@ -27,11 +27,6 @@ const ModalPermanentTokenEdit = (props) => { [key]: e.target.value }) - const copyInput = (e) => { - e.target.select() - document.execCommand('copy') - } - const updatePermanentToken = (e) => { e.preventDefault() props.updatePermanentToken(props.id, inputs).then(props.closeModal) @@ -73,7 +68,7 @@ const ModalPermanentTokenEdit = (props) => { readOnly: true, placeholder: 'Permanent token id', value: props.id, - onFocus: copyInput + copyOnFocus: true }) ), diff --git a/src/ui/scripts/components/overlays/OverlayFailure.js b/src/ui/scripts/components/overlays/OverlayFailure.js index ba538a35..d68d5414 100644 --- a/src/ui/scripts/components/overlays/OverlayFailure.js +++ b/src/ui/scripts/components/overlays/OverlayFailure.js @@ -36,7 +36,6 @@ const OverlayFailure = (props) => { h(Message, { status: 'error' }, `Please report this issue on GitHub if you can't resolve it by yourself.`), h(Textarea, { - readOnly: true, rows: 6, value: formatErrors(props.errors) }), diff --git a/src/ui/styles/_inputMessage.scss b/src/ui/styles/_inputMessage.scss new file mode 100644 index 00000000..e74d43db --- /dev/null +++ b/src/ui/styles/_inputMessage.scss @@ -0,0 +1,51 @@ +.inputMessage { + + display: grid; + grid-template-areas: 'main'; + position: relative; + margin-bottom: $formbase__margin; + + @keyframes inputMessage__show { + 0% { + opacity: 1; + } + 60% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + + .input { + grid-area: main; + margin-bottom: 0; + } + + &::after { + content: attr(title); + grid-area: main; + display: flex; + align-items: center; + justify-content: center; + background: rgba($context, .98); + border: 2px solid $primary; + border-radius: $formbase__radius; + color: $white; + font-size: .9em; + opacity: 0; + pointer-events: none; + + @supports (backdrop-filter: blur(10px)) { + background: $context; + backdrop-filter: blur(10px); + } + } + + &:focus-within::after { + animation-name: inputMessage__show; + animation-timing-function: ease; + animation-duration: 2s; + } + +} \ No newline at end of file diff --git a/src/ui/styles/_vars.scss b/src/ui/styles/_vars.scss index 95f1375f..0318c7dc 100644 --- a/src/ui/styles/_vars.scss +++ b/src/ui/styles/_vars.scss @@ -28,6 +28,7 @@ $shadow: 0 2px 10px rgba(0, 0, 0, .12); // formbase ---------------------------------------------------- // $formbase__radius: $innerRadius; +$formbase__margin: .9rem; $formbase__background: $dimmed; $formbase__svg: #fff; $formbase__color: $white; diff --git a/src/ui/styles/index.scss b/src/ui/styles/index.scss index a026c46b..529d9f7c 100644 --- a/src/ui/styles/index.scss +++ b/src/ui/styles/index.scss @@ -17,6 +17,7 @@ @import 'input'; @import 'select'; @import 'control'; +@import 'inputMessage'; @import 'main'; @import 'card'; @import 'spinner'; From 3f698a20841d685137bdff7475fc1ff63c5fc0e2 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 20 Dec 2020 12:45:16 +0100 Subject: [PATCH 106/208] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01b22ec2..a725dc73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased (Events)] +### Added + +- "Copied to clipboard" message when clicking on an input or textarea that copies to the clipboard (#166) + ### Fixed - Close, delete and submit in modals could be triggered multiple times From eb36c0f110a29bbffd2da03fa78b37bb43538490 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 20 Dec 2020 14:30:43 +0100 Subject: [PATCH 107/208] Adjsut font size --- src/ui/styles/_inputMessage.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/styles/_inputMessage.scss b/src/ui/styles/_inputMessage.scss index e74d43db..8d821f42 100644 --- a/src/ui/styles/_inputMessage.scss +++ b/src/ui/styles/_inputMessage.scss @@ -32,7 +32,7 @@ border: 2px solid $primary; border-radius: $formbase__radius; color: $white; - font-size: .9em; + font-size: .86em; opacity: 0; pointer-events: none; From c416c3821bea68b0120fa0b29d2d0cb67a5350d2 Mon Sep 17 00:00:00 2001 From: Evan Schwartz <3262610+emschwartz@users.noreply.github.com> Date: Tue, 22 Dec 2020 09:18:44 -0500 Subject: [PATCH 108/208] fix: build & start separately on Heroku --- Procfile | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..4f311545 --- /dev/null +++ b/Procfile @@ -0,0 +1,3 @@ +release: npm run build +web: npm run server + From fc67bad4c26f37951fb255821cbcfa6fb7ad1a18 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Thu, 24 Dec 2020 17:56:53 +0100 Subject: [PATCH 109/208] Improve imports for rollup --- src/ui/scripts/components/overlays/OverlayFailure.js | 2 +- src/ui/scripts/components/overlays/OverlayLogin.js | 2 +- src/ui/scripts/components/routes/RouteSettings.js | 2 +- src/ui/scripts/index.js | 4 ++-- src/ui/scripts/utils/storage.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/scripts/components/overlays/OverlayFailure.js b/src/ui/scripts/components/overlays/OverlayFailure.js index d68d5414..48acca7b 100644 --- a/src/ui/scripts/components/overlays/OverlayFailure.js +++ b/src/ui/scripts/components/overlays/OverlayFailure.js @@ -1,7 +1,7 @@ import { createElement as h } from 'react' import PropTypes from 'prop-types' -import { homepage } from '../../../../../package' +import { homepage } from '../../../../../package.json' import formatErrors from '../../utils/formatErrors' import * as storage from '../../utils/storage' diff --git a/src/ui/scripts/components/overlays/OverlayLogin.js b/src/ui/scripts/components/overlays/OverlayLogin.js index ccec80dc..8b093403 100644 --- a/src/ui/scripts/components/overlays/OverlayLogin.js +++ b/src/ui/scripts/components/overlays/OverlayLogin.js @@ -1,7 +1,7 @@ import { createElement as h, useState } from 'react' import PropTypes from 'prop-types' -import { homepage } from '../../../../../package' +import { homepage } from '../../../../../package.json' import isDemoMode from '../../../../utils/isDemoMode' import Input from '../Input' diff --git a/src/ui/scripts/components/routes/RouteSettings.js b/src/ui/scripts/components/routes/RouteSettings.js index a9d20427..71af42da 100644 --- a/src/ui/scripts/components/routes/RouteSettings.js +++ b/src/ui/scripts/components/routes/RouteSettings.js @@ -1,6 +1,6 @@ import { createElement as h, Fragment, useEffect } from 'react' -import { version, homepage } from '../../../../../package' +import { version, homepage } from '../../../../../package.json' import { MODALS_DOMAIN_ADD, MODALS_DOMAIN_EDIT, diff --git a/src/ui/scripts/index.js b/src/ui/scripts/index.js index 36ad7b3b..4a81a06c 100644 --- a/src/ui/scripts/index.js +++ b/src/ui/scripts/index.js @@ -8,8 +8,8 @@ import isDemoMode from '../../utils/isDemoMode' import enhanceState from './enhancers/enhanceState' import createStore from './utils/createStore' import * as storage from './utils/storage' -import reducers from './reducers' -import * as actions from './actions' +import reducers from './reducers/index' +import * as actions from './actions/index' import { initialState as initialTokenState } from './reducers/token' import { initialState as initialRouteState } from './reducers/route' diff --git a/src/ui/scripts/utils/storage.js b/src/ui/scripts/utils/storage.js index 734f9587..5ac71113 100644 --- a/src/ui/scripts/utils/storage.js +++ b/src/ui/scripts/utils/storage.js @@ -1,4 +1,4 @@ -import { version } from '../../../../package' +import { version } from '../../../../package.json' // Should include the package version so we can increase the version number // when the structure of the state has changed to avoid loading outdated states. From 410e1a79c6db038e130404a119d83daed4e0136f Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Fri, 25 Dec 2020 13:26:05 +0100 Subject: [PATCH 110/208] Update deps --- package.json | 8 +-- yarn.lock | 186 ++++++++++++++++++--------------------------------- 2 files changed, 68 insertions(+), 126 deletions(-) diff --git a/package.json b/package.json index bded89d7..3348790e 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,8 @@ }, "dependencies": { "ackee-tracker": "^4.2.0", - "apollo-server-lambda": "^2.19.0", - "apollo-server-micro": "^2.19.0", + "apollo-server-lambda": "^2.19.1", + "apollo-server-micro": "^2.19.1", "apollo-server-plugin-http-headers": "^0.1.4", "classnames": "^2.2.6", "date-fns": "^2.16.1", @@ -45,7 +45,7 @@ "dotenv": "^8.2.0", "formbase": "^12.0.1", "graphql": "^15.4.0", - "graphql-scalars": "^1.6.1", + "graphql-scalars": "^1.7.0", "graphql-tools": "^7.0.2", "human-number": "^1.0.6", "immer": "^8.0.0", @@ -69,7 +69,7 @@ "redux-devtools-extension": "^2.13.8", "redux-thunk": "^2.3.0", "request-ip": "^2.1.3", - "rosid-handler-js": "^13.0.1", + "rosid-handler-js": "^13.0.2", "rosid-handler-sass": "^8.0.0", "s-ago": "^2.2.0", "sanitize-filename": "^1.6.3", diff --git a/yarn.lock b/yarn.lock index fd4fcf70..48d630ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1471,14 +1471,6 @@ dependencies: "@types/node" "*" -"@types/glob@^7.1.1": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - "@types/graphql-upload@^8.0.0": version "8.0.4" resolved "https://registry.yarnpkg.com/@types/graphql-upload/-/graphql-upload-8.0.4.tgz#23a8ffb3d2fe6e0ee07e6f16ee9d9d5e995a2f4f" @@ -1545,11 +1537,6 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== -"@types/minimatch@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== - "@types/mongodb@^3.5.27": version "3.6.3" resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.6.3.tgz#5655af409d9e32d5d5ae9a653abf3e5f9c83eb7a" @@ -1805,13 +1792,13 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -apollo-cache-control@^0.11.4: - version "0.11.4" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.11.4.tgz#06d57d728e6f928e03b9cc3b993f6102f305c32e" - integrity sha512-FUKE8ASr8GxVq5rmky/tY8bsf++cleGT591lfLiqnPsP1fo3kAfgRfWA2QRHTCKFNlQxzUhVOEDv+PaysqiOjw== +apollo-cache-control@^0.11.5: + version "0.11.5" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.11.5.tgz#de208bd6fef440c1fed451b830d6402eab307a2e" + integrity sha512-jvarfQhwDRazpOsmkt5Pd7qGFrtbL70zMbUZGqDhJSYpfqI672f7bXXc7O3vtpbD3qnS3XTBvK2kspX/Bdo0IA== dependencies: apollo-server-env "^2.4.5" - apollo-server-plugin-base "^0.10.2" + apollo-server-plugin-base "^0.10.3" apollo-datasource@^0.7.2: version "0.7.2" @@ -1849,10 +1836,10 @@ apollo-link@^1.2.14: tslib "^1.9.3" zen-observable-ts "^0.8.21" -apollo-reporting-protobuf@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.6.1.tgz#09294e5f5f6b2285eb94b40621ed42113eaabea3" - integrity sha512-qr4DheFP154PGZsd93SSIS9RkqHnR5b6vT+eCloWjy3UIpY+yZ3cVLlttlIjYvOG4xTJ25XEwcHiAExatQo/7g== +apollo-reporting-protobuf@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.6.2.tgz#5572866be9b77f133916532b10e15fbaa4158304" + integrity sha512-WJTJxLM+MRHNUxt1RTl4zD0HrLdH44F2mDzMweBj1yHL0kSt8I1WwoiF/wiGVSpnG48LZrBegCaOJeuVbJTbtw== dependencies: "@apollo/protobufjs" "^1.0.3" @@ -1863,28 +1850,28 @@ apollo-server-caching@^0.5.2: dependencies: lru-cache "^5.0.0" -apollo-server-core@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.19.0.tgz#ff14e788f228c2d6739478a68cf93f46a16e5bfa" - integrity sha512-2aMKUVPyNbomJQaG2tkpfqvp1Tfgxgkdr7nX5zHudYNSzsPrHw+CcYlCbIVFFI/mTZsjoK9czNq1qerFRxZbJw== +apollo-server-core@^2.19.1: + version "2.19.1" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.19.1.tgz#18cb74004297fe61689862b8faf00b30a5a30ce7" + integrity sha512-5EVmcY8Ij7Ywwu+Ze4VaUhZBcxl8t5ztcSatJrKMd4HYlEHyaNGBV2itfpyqAthxfdMbGKqlpeCHmTGSqDcNpA== dependencies: "@apollographql/apollo-tools" "^0.4.3" "@apollographql/graphql-playground-html" "1.6.26" "@types/graphql-upload" "^8.0.0" "@types/ws" "^7.0.0" - apollo-cache-control "^0.11.4" + apollo-cache-control "^0.11.5" apollo-datasource "^0.7.2" apollo-graphql "^0.6.0" - apollo-reporting-protobuf "^0.6.1" + apollo-reporting-protobuf "^0.6.2" apollo-server-caching "^0.5.2" apollo-server-env "^2.4.5" apollo-server-errors "^2.4.2" - apollo-server-plugin-base "^0.10.2" - apollo-server-types "^0.6.1" - apollo-tracing "^0.12.0" + apollo-server-plugin-base "^0.10.3" + apollo-server-types "^0.6.2" + apollo-tracing "^0.12.1" async-retry "^1.2.1" fast-json-stable-stringify "^2.0.0" - graphql-extensions "^0.12.6" + graphql-extensions "^0.12.7" graphql-tag "^2.9.2" graphql-tools "^4.0.0" graphql-upload "^8.0.2" @@ -1908,35 +1895,35 @@ apollo-server-errors@^2.4.2: resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.4.2.tgz#1128738a1d14da989f58420896d70524784eabe5" integrity sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ== -apollo-server-lambda@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/apollo-server-lambda/-/apollo-server-lambda-2.19.0.tgz#63d114bd095ca6d2958e1d05f0c0ed9d23cfd61b" - integrity sha512-44W6QaHMnVsKKXtC0o04JR0U65hoUC7Pfj3ik1PThsGejHVByyaQi7Qfbas3vuMAxU0UGkF5TKycc7K6KFxGUw== +apollo-server-lambda@^2.19.1: + version "2.19.1" + resolved "https://registry.yarnpkg.com/apollo-server-lambda/-/apollo-server-lambda-2.19.1.tgz#f65171185803d9a75b5f581265a5d57898836d8f" + integrity sha512-NBBcw53uNG/trnDcAKxHQFnt2RnpkBdDChgE50alZ8KBHIA0YknTFodNBkJB797cN0fbmV0qKwcGGYA6LSw4cA== dependencies: "@apollographql/graphql-playground-html" "1.6.26" "@types/aws-lambda" "^8.10.31" - apollo-server-core "^2.19.0" + apollo-server-core "^2.19.1" apollo-server-env "^2.4.5" - apollo-server-types "^0.6.1" + apollo-server-types "^0.6.2" graphql-tools "^4.0.0" -apollo-server-micro@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/apollo-server-micro/-/apollo-server-micro-2.19.0.tgz#911a397d234340c7ab4c9abb242c74503897849e" - integrity sha512-EvGzcdb9DM71NmR4plF8Y2l18uE30uaElb1LPCV57/7ZN0+IvX8xNPxXP9geoyngniPiZVP1kqiRKdq1OPAu4g== +apollo-server-micro@^2.19.1: + version "2.19.1" + resolved "https://registry.yarnpkg.com/apollo-server-micro/-/apollo-server-micro-2.19.1.tgz#135491582c17274052995eabe069355231422244" + integrity sha512-zHj4m86HHasqt8423JwR4xwAokuF7pdE/YLDdghy2+t5AfxRDjPoJOYqFK77Yk71tld+9bmeFTus82/H7hDgdQ== dependencies: "@apollographql/graphql-playground-html" "1.6.26" accept "^3.0.2" - apollo-server-core "^2.19.0" - apollo-server-types "^0.6.1" + apollo-server-core "^2.19.1" + apollo-server-types "^0.6.2" micro "^9.3.2" -apollo-server-plugin-base@^0.10.2: - version "0.10.2" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.10.2.tgz#185aea98ba22afe275fb01659070edeb480a89a7" - integrity sha512-uM5uL1lOxbXdgvt/aEIbgs40fV9xA45Y3Mmh0VtQ/ddqq0MXR5aG92nnf8rM+URarBCUfxKJKaYzJJ/CXAnEdA== +apollo-server-plugin-base@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.10.3.tgz#95099371b59766963742b97a165fe89f7bd268dc" + integrity sha512-NCLOsk9Jsd8oLvefkQvROdMDQvnHnzbzz3MPCqEYjCOEv0YBM8T77D0wCwbcViDS2M5p0W6un2ub9s/vU71f7Q== dependencies: - apollo-server-types "^0.6.1" + apollo-server-types "^0.6.2" apollo-server-plugin-http-headers@^0.1.4: version "0.1.4" @@ -1945,22 +1932,22 @@ apollo-server-plugin-http-headers@^0.1.4: dependencies: cookie "*" -apollo-server-types@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.6.1.tgz#61486980b44cacee2cb4939f0b369a0eb661a098" - integrity sha512-IEQ37aYvMLiTUzsySVLOSuvvhxuyYdhI05f3cnH6u2aN1HgGp7vX6bg+U3Ue8wbHfdcifcGIk5UEU+Q+QO6InA== +apollo-server-types@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.6.2.tgz#3e97d26c580c5e4bd1cc50dbfe9d2ac26b812ab5" + integrity sha512-LgSKgAStiDzpUSLYwJoAmy0W8nkxx/ExMmgEPgEYVi6dKPkUmtu561J970PhGdYH+D79ke3g87D+plkUkgfnlQ== dependencies: - apollo-reporting-protobuf "^0.6.1" + apollo-reporting-protobuf "^0.6.2" apollo-server-caching "^0.5.2" apollo-server-env "^2.4.5" -apollo-tracing@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.12.0.tgz#26250d7789c28aa89d63226eb674706dd69a568a" - integrity sha512-cMUYGE6mOEwb9HDqhf4fiPEo2JMhjPIqEprAQEC57El76avRpRig5NM0bnqMZcYJZR5QmLlNcttNccOwf9WrNg== +apollo-tracing@^0.12.1: + version "0.12.1" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.12.1.tgz#5edfa42e53d4b5ce60a1649f1b336d7e29a27583" + integrity sha512-qdkUjW+pOaidGOSITypeYE288y28HkPmGNpUtyQSOeTxgqXHtQX3TDWiOJ2SmrLH08xdSwfvz9o5KrTq4PdISg== dependencies: apollo-server-env "^2.4.5" - apollo-server-plugin-base "^0.10.2" + apollo-server-plugin-base "^0.10.3" apollo-upload-client@14.1.2: version "14.1.2" @@ -3448,20 +3435,6 @@ defined@^1.0.0: resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= -del@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/del/-/del-5.1.0.tgz#d9487c94e367410e6eff2925ee58c0c84a75b3a7" - integrity sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA== - dependencies: - globby "^10.0.1" - graceful-fs "^4.2.2" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.1" - p-map "^3.0.0" - rimraf "^3.0.0" - slash "^3.0.0" - del@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" @@ -4059,7 +4032,7 @@ fast-diff@^1.2.0: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== -fast-glob@^3.0.3, fast-glob@^3.1.1: +fast-glob@^3.1.1: version "3.2.4" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== @@ -4296,16 +4269,6 @@ fsevents@~2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== -fsify@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/fsify/-/fsify-4.0.1.tgz#9094dda54d4d6c045bfe30bf0eb954972c206b2b" - integrity sha512-4y4QNEpPy1mcz2jNFqxkx00WWEmelbo7MqCJMQgsHprjeDH8eEC/XYJM+AGJnXF0hFmTEF5j0mklofKxJKGwww== - dependencies: - del "^5.0.0" - is-path-inside "^3.0.0" - is-plain-obj "^2.0.0" - slash "^3.0.0" - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -4421,20 +4384,6 @@ globby@11.0.1, globby@^11.0.1: merge2 "^1.3.0" slash "^3.0.0" -globby@^10.0.1: - version "10.0.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" - integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg== - dependencies: - "@types/glob" "^7.1.1" - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.0.3" - glob "^7.1.3" - ignore "^5.1.1" - merge2 "^1.2.3" - slash "^3.0.0" - got@^9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -4452,24 +4401,24 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: +graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -graphql-extensions@^0.12.6: - version "0.12.6" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.12.6.tgz#c66be43035662a8cfb0b8efe9df96595338bd13c" - integrity sha512-EUNw+OIRXYTPxToSoJjhJvS5aGa94KkdkZnL1I9DCZT64/+rzQNeLeGj+goj2RYuYvoQe1Bmcx0CNZ1GqwBhng== +graphql-extensions@^0.12.7: + version "0.12.7" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.12.7.tgz#9732cb50090eee6ca9226ad9349a4308668a1fa5" + integrity sha512-yc9qOmEmWVZNkux9m0eCiHdtYSwNZRHkFhgfKfDn4u/gpsJolft1iyMUADnG/eytiRW0CGZFBpZjHkJhpginuQ== dependencies: "@apollographql/apollo-tools" "^0.4.3" apollo-server-env "^2.4.5" - apollo-server-types "^0.6.1" + apollo-server-types "^0.6.2" -graphql-scalars@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/graphql-scalars/-/graphql-scalars-1.6.1.tgz#15564239a6142b9e9e6b6ce01a1b50ba42ebfd0f" - integrity sha512-PfgFeAioO48vziGD2SM7KgY0cmUyM9yh1BF2L12muFXIvHbIxYc5JYFSfjqtNIRRuHt00KD29pdFdV+1lLMvuw== +graphql-scalars@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/graphql-scalars/-/graphql-scalars-1.7.0.tgz#32cf623027664f1d50c5d058767a87387475c107" + integrity sha512-CCL9aaZdK1VD2jYFCF0ONB1cT5brK9DXymSuu/E/JoYLmAaLwGgjdzY66mT2ZI5oOIrSrJqWTf4y20qdSaE4MQ== dependencies: tslib "~2.0.3" @@ -4774,7 +4723,7 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1, ignore@^5.1.4: +ignore@^5.1.4: version "5.1.8" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== @@ -5073,16 +5022,11 @@ is-path-cwd@^2.2.0: resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== -is-path-inside@^3.0.0, is-path-inside@^3.0.1, is-path-inside@^3.0.2: +is-path-inside@^3.0.1, is-path-inside@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== -is-plain-obj@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - is-plain-object@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" @@ -5646,7 +5590,7 @@ memory-pager@^1.0.2: resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== -merge2@^1.2.3, merge2@^1.3.0: +merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -7427,20 +7371,18 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rosid-handler-js@^13.0.1: - version "13.0.1" - resolved "https://registry.yarnpkg.com/rosid-handler-js/-/rosid-handler-js-13.0.1.tgz#3f5653091913e2bc3c607e847bdfde03fdd6343c" - integrity sha512-uwkOZMZ7O2S7iRokR2J7cRafI4hdJIzgtCeupFuW23UFQ4KiozUB14xDRt1wtX5CNljEVzT9Q8ZEKYocwssK7g== +rosid-handler-js@^13.0.2: + version "13.0.2" + resolved "https://registry.yarnpkg.com/rosid-handler-js/-/rosid-handler-js-13.0.2.tgz#85cdaab8688248f1b637f3ac7f3cb5dc34673ba9" + integrity sha512-oGcyJvwyfeqclFPgexLfKGKCwOnvY0P5h5/muzYRw3HaoPsm+F+igyhcFDsPYfl6EV9ILy1+BQCvsN0rstDjqw== dependencies: "@babel/core" "^7.8.4" "@babel/preset-env" "^7.8.4" "@babel/preset-react" "^7.8.3" babelify "^10.0.0" browserify "^17.0.0" - fsify "^4.0.0" loose-envify "^1.4.0" terser "^5.3.5" - uuid "^8.0.0" rosid-handler-sass@^8.0.0: version "8.0.0" From 2396806819ccaeb955f53e7bfa1cfa9c80245603 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Fri, 25 Dec 2020 15:44:34 +0100 Subject: [PATCH 111/208] Fix circular dependencies --- src/ui/scripts/components/Modals.js | 7 ------- src/ui/scripts/components/modals/ModalDomainAdd.js | 4 ++-- src/ui/scripts/components/modals/ModalDomainEdit.js | 4 ++-- src/ui/scripts/components/modals/ModalEventAdd.js | 4 ++-- src/ui/scripts/components/modals/ModalEventEdit.js | 4 ++-- src/ui/scripts/components/modals/ModalPermanentTokenAdd.js | 4 ++-- .../scripts/components/modals/ModalPermanentTokenEdit.js | 4 ++-- src/ui/scripts/utils/commonModalProps.js | 7 +++++++ 8 files changed, 19 insertions(+), 19 deletions(-) create mode 100644 src/ui/scripts/utils/commonModalProps.js diff --git a/src/ui/scripts/components/Modals.js b/src/ui/scripts/components/Modals.js index a4b9a7b3..327d0f6e 100644 --- a/src/ui/scripts/components/Modals.js +++ b/src/ui/scripts/components/Modals.js @@ -1,5 +1,4 @@ import { createElement as h, Fragment } from 'react' -import PropTypes from 'prop-types' import { MODALS_DOMAIN_ADD, @@ -18,12 +17,6 @@ import ModalEventEdit from './modals/ModalEventEdit' import ModalPermanentTokenAdd from './modals/ModalPermanentTokenAdd' import ModalPermanentTokenEdit from './modals/ModalPermanentTokenEdit' -export const commonPropTypes = { - current: PropTypes.bool.isRequired, - active: PropTypes.bool.isRequired, - closeModal: PropTypes.func.isRequired -} - const Modals = (props) => { const modals = Object.entries(props.modals.value).map(([ modalId, modalData ], index, modals) => { diff --git a/src/ui/scripts/components/modals/ModalDomainAdd.js b/src/ui/scripts/components/modals/ModalDomainAdd.js index cc69589b..9e26976b 100644 --- a/src/ui/scripts/components/modals/ModalDomainAdd.js +++ b/src/ui/scripts/components/modals/ModalDomainAdd.js @@ -6,8 +6,8 @@ import Input from '../Input' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' -import { commonPropTypes } from '../Modals' +import commonModalProps from '../../utils/commonModalProps' import shortId from '../../utils/shortId' const ModalDomainAdd = (props) => { @@ -79,7 +79,7 @@ const ModalDomainAdd = (props) => { } ModalDomainAdd.propTypes = { - ...commonPropTypes, + ...commonModalProps, fetching: PropTypes.bool.isRequired, addDomain: PropTypes.func.isRequired } diff --git a/src/ui/scripts/components/modals/ModalDomainEdit.js b/src/ui/scripts/components/modals/ModalDomainEdit.js index 816bb304..55c02862 100644 --- a/src/ui/scripts/components/modals/ModalDomainEdit.js +++ b/src/ui/scripts/components/modals/ModalDomainEdit.js @@ -7,9 +7,9 @@ import Textarea from '../Textarea' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' -import { commonPropTypes } from '../Modals' import customTracker from '../../../../utils/customTracker' +import commonModalProps from '../../utils/commonModalProps' import shortId from '../../utils/shortId' const ModalDomainEdit = (props) => { @@ -125,7 +125,7 @@ const ModalDomainEdit = (props) => { } ModalDomainEdit.propTypes = { - ...commonPropTypes, + ...commonModalProps, id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, fetching: PropTypes.bool.isRequired, diff --git a/src/ui/scripts/components/modals/ModalEventAdd.js b/src/ui/scripts/components/modals/ModalEventAdd.js index fcc44acf..ad233647 100644 --- a/src/ui/scripts/components/modals/ModalEventAdd.js +++ b/src/ui/scripts/components/modals/ModalEventAdd.js @@ -9,8 +9,8 @@ import Select from '../Select' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' -import { commonPropTypes } from '../Modals' +import commonModalProps from '../../utils/commonModalProps' import shortId from '../../utils/shortId' const ModalEventAdd = (props) => { @@ -104,7 +104,7 @@ const ModalEventAdd = (props) => { } ModalEventAdd.propTypes = { - ...commonPropTypes, + ...commonModalProps, fetching: PropTypes.bool.isRequired, addEvent: PropTypes.func.isRequired } diff --git a/src/ui/scripts/components/modals/ModalEventEdit.js b/src/ui/scripts/components/modals/ModalEventEdit.js index 9163e1b6..4d0014a7 100644 --- a/src/ui/scripts/components/modals/ModalEventEdit.js +++ b/src/ui/scripts/components/modals/ModalEventEdit.js @@ -10,8 +10,8 @@ import Textarea from '../Textarea' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' -import { commonPropTypes } from '../Modals' +import commonModalProps from '../../utils/commonModalProps' import shortId from '../../utils/shortId' const ModalEventEdit = (props) => { @@ -145,7 +145,7 @@ const ModalEventEdit = (props) => { } ModalEventEdit.propTypes = { - ...commonPropTypes, + ...commonModalProps, id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, fetching: PropTypes.bool.isRequired, diff --git a/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js b/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js index b77a236d..4ba2ae03 100644 --- a/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js +++ b/src/ui/scripts/components/modals/ModalPermanentTokenAdd.js @@ -6,8 +6,8 @@ import Input from '../Input' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' -import { commonPropTypes } from '../Modals' +import commonModalProps from '../../utils/commonModalProps' import shortId from '../../utils/shortId' const ModalPermanentTokenAdd = (props) => { @@ -79,7 +79,7 @@ const ModalPermanentTokenAdd = (props) => { } ModalPermanentTokenAdd.propTypes = { - ...commonPropTypes, + ...commonModalProps, fetching: PropTypes.bool.isRequired, addPermanentToken: PropTypes.func.isRequired } diff --git a/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js b/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js index b16c8591..70fb214e 100644 --- a/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js +++ b/src/ui/scripts/components/modals/ModalPermanentTokenEdit.js @@ -6,8 +6,8 @@ import Input from '../Input' import Label from '../Label' import Spinner from '../Spinner' import Spacer from '../Spacer' -import { commonPropTypes } from '../Modals' +import commonModalProps from '../../utils/commonModalProps' import shortId from '../../utils/shortId' const ModalPermanentTokenEdit = (props) => { @@ -108,7 +108,7 @@ const ModalPermanentTokenEdit = (props) => { } ModalPermanentTokenEdit.propTypes = { - ...commonPropTypes, + ...commonModalProps, id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, fetching: PropTypes.bool.isRequired, diff --git a/src/ui/scripts/utils/commonModalProps.js b/src/ui/scripts/utils/commonModalProps.js new file mode 100644 index 00000000..5890fb0d --- /dev/null +++ b/src/ui/scripts/utils/commonModalProps.js @@ -0,0 +1,7 @@ +import PropTypes from 'prop-types' + +export default { + current: PropTypes.bool.isRequired, + active: PropTypes.bool.isRequired, + closeModal: PropTypes.func.isRequired +} \ No newline at end of file From 1a297ecaa1849fc1612af04a99f3edd252a42532 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Fri, 25 Dec 2020 16:01:18 +0100 Subject: [PATCH 112/208] Textadrea should be read only --- src/ui/scripts/components/overlays/OverlayFailure.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/scripts/components/overlays/OverlayFailure.js b/src/ui/scripts/components/overlays/OverlayFailure.js index 48acca7b..b468b06b 100644 --- a/src/ui/scripts/components/overlays/OverlayFailure.js +++ b/src/ui/scripts/components/overlays/OverlayFailure.js @@ -37,7 +37,8 @@ const OverlayFailure = (props) => { h(Textarea, { rows: 6, - value: formatErrors(props.errors) + value: formatErrors(props.errors), + readOnly: true }), h(Spacer, { size: 1 }) From 2a5bb69d1b52c7170d01fb8b6eeb9c0e04af0863 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Fri, 25 Dec 2020 16:01:37 +0100 Subject: [PATCH 113/208] Use new build tools to reduce size of bundle --- build.js | 30 +- package.json | 2 +- yarn.lock | 1439 +++++++++++++++++--------------------------------- 3 files changed, 479 insertions(+), 992 deletions(-) diff --git a/build.js b/build.js index d247afae..65cd178a 100644 --- a/build.js +++ b/build.js @@ -5,7 +5,7 @@ require('dotenv').config() const { resolve } = require('path') const { writeFile, readFile } = require('fs').promises const sass = require('rosid-handler-sass') -const js = require('rosid-handler-js') +const js = require('rosid-handler-js-next') const html = require('./src/ui/index') const isDemoMode = require('./src/utils/isDemoMode') @@ -43,32 +43,14 @@ const scripts = async () => { const filePath = resolve(__dirname, 'src/ui/scripts/index.js') - const babel = { - presets: [ - [ - '@babel/preset-env', { - targets: { - browsers: [ - 'last 2 Safari versions', - 'last 2 Chrome versions', - 'last 2 Opera versions', - 'last 2 Firefox versions' - ] - } - } - ] - ], - babelrc: false - } - const data = js(filePath, { optimize: isDevelopmentMode === false, - env: { - ACKEE_TRACKER: process.env.ACKEE_TRACKER, - ACKEE_DEMO: isDemoMode === true ? 'true' : 'false', - NODE_ENV: isDevelopmentMode === true ? 'development' : 'production' + replace: { + 'process.env.ACKEE_TRACKER': JSON.stringify(process.env.ACKEE_TRACKER), + 'process.env.ACKEE_DEMO': JSON.stringify(isDemoMode === true ? 'true' : 'false') }, - babel + nodeGlobals: true, + babel: false }) return data diff --git a/package.json b/package.json index 3348790e..7440de85 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "redux-devtools-extension": "^2.13.8", "redux-thunk": "^2.3.0", "request-ip": "^2.1.3", - "rosid-handler-js": "^13.0.2", + "rosid-handler-js-next": "^1.0.0", "rosid-handler-sass": "^8.0.0", "s-ago": "^2.2.0", "sanitize-filename": "^1.6.3", diff --git a/yarn.lock b/yarn.lock index 48d630ce..5cfc0126 100644 --- a/yarn.lock +++ b/yarn.lock @@ -68,12 +68,19 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/compat-data@^7.12.1", "@babel/compat-data@^7.12.5": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.5.tgz#f56db0c4bb1bbbf221b4e81345aab4141e7cb0e9" - integrity sha512-DTsS7cxrsH3by8nqQSpFSyjSfSYl57D6Cf4q8dW3LK83tBKBDCkfcay1nYkXq1nIHXnpX8WMMb/O25HOy3h1zg== +"@babel/code-frame@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41" + integrity sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw== -"@babel/core@^7.0.0", "@babel/core@^7.7.5", "@babel/core@^7.8.4": +"@babel/core@^7.0.0", "@babel/core@^7.7.5": version "7.12.3" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.3.tgz#1b436884e1e3bff6fb1328dc02b208759de92ad8" integrity sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g== @@ -95,6 +102,27 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/core@^7.8.4": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd" + integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/generator" "^7.12.10" + "@babel/helper-module-transforms" "^7.12.1" + "@babel/helpers" "^7.12.5" + "@babel/parser" "^7.12.10" + "@babel/template" "^7.12.7" + "@babel/traverse" "^7.12.10" + "@babel/types" "^7.12.10" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.2" + lodash "^4.17.19" + semver "^5.4.1" + source-map "^0.5.0" + "@babel/generator@^7.12.1", "@babel/generator@^7.12.5", "@babel/generator@^7.5.0": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de" @@ -104,6 +132,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.10", "@babel/generator@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af" + integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA== + dependencies: + "@babel/types" "^7.12.11" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" @@ -111,6 +148,13 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-annotate-as-pure@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d" + integrity sha512-XplmVbC1n+KY6jL8/fgLVXXUauDIB+lD5+GsQEh6F6GBF1dq1qy4DP4yXWzDKcoqXB3X58t61e85Fitoww4JVQ== + dependencies: + "@babel/types" "^7.12.10" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz#bb0b75f31bf98cbf9ff143c1ae578b87274ae1a3" @@ -136,7 +180,7 @@ "@babel/helper-annotate-as-pure" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helper-compilation-targets@^7.12.1": +"@babel/helper-compilation-targets@^7.12.5": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz#cb470c76198db6a24e9dbc8987275631e5d29831" integrity sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw== @@ -158,12 +202,11 @@ "@babel/helper-split-export-declaration" "^7.10.4" "@babel/helper-create-regexp-features-plugin@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.1.tgz#18b1302d4677f9dc4740fe8c9ed96680e29d37e8" - integrity sha512-rsZ4LGvFTZnzdNZR5HZdmJVuXK8834R5QkF3WvcnBhrlVtF0HSIUC6zbreL9MgjTywhKokn8RIYRiq99+DLAxA== + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz#2084172e95443fa0a09214ba1bb328f9aea1278f" + integrity sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ== dependencies: "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-regex" "^7.10.4" regexpu-core "^4.7.1" "@babel/helper-define-map@^7.10.4": @@ -191,6 +234,15 @@ "@babel/template" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/helper-function-name@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42" + integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA== + dependencies: + "@babel/helper-get-function-arity" "^7.12.10" + "@babel/template" "^7.12.7" + "@babel/types" "^7.12.11" + "@babel/helper-get-function-arity@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" @@ -198,6 +250,13 @@ dependencies: "@babel/types" "^7.10.4" +"@babel/helper-get-function-arity@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf" + integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag== + dependencies: + "@babel/types" "^7.12.10" + "@babel/helper-hoist-variables@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e" @@ -212,7 +271,7 @@ dependencies: "@babel/types" "^7.12.1" -"@babel/helper-module-imports@^7.12.1": +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.5": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA== @@ -246,13 +305,6 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== -"@babel/helper-regex@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" - integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== - dependencies: - lodash "^4.17.19" - "@babel/helper-remap-async-to-generator@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz#8c4dbbf916314f6047dc05e6a2217074238347fd" @@ -293,15 +345,27 @@ dependencies: "@babel/types" "^7.11.0" +"@babel/helper-split-export-declaration@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a" + integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g== + dependencies: + "@babel/types" "^7.12.11" + "@babel/helper-validator-identifier@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== -"@babel/helper-validator-option@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz#175567380c3e77d60ff98a54bb015fe78f2178d9" - integrity sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A== +"@babel/helper-validator-identifier@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" + integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== + +"@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f" + integrity sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw== "@babel/helper-wrap-function@^7.10.4": version "7.12.3" @@ -313,7 +377,7 @@ "@babel/traverse" "^7.10.4" "@babel/types" "^7.10.4" -"@babel/helpers@^7.12.1": +"@babel/helpers@^7.12.1", "@babel/helpers@^7.12.5": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA== @@ -341,10 +405,15 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0" integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ== +"@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79" + integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg== + "@babel/plugin-proposal-async-generator-functions@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz#dc6c1170e27d8aca99ff65f4925bd06b1c90550e" - integrity sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A== + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz#04b8f24fd4532008ab4e79f788468fd5a8476566" + integrity sha512-nrz9y0a4xmUrRq51bYkWJIO5SBZyG2ys2qinHsN0zHDHVsUaModrkpyWWWXfGqYQmOL3x9sQIcTNN/pBGpo09A== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-remap-async-to-generator" "^7.12.1" @@ -398,10 +467,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" -"@babel/plugin-proposal-numeric-separator@^7.12.1": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.5.tgz#b1ce757156d40ed79d59d467cb2b154a5c4149ba" - integrity sha512-UiAnkKuOrCyjZ3sYNHlRlfuZJbBHknMQ9VMwVeX97Ofwx7RpD6gS2HfqTCh8KNUQgcOm8IKt103oR4KIjh7Q8g== +"@babel/plugin-proposal-numeric-separator@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz#8bf253de8139099fea193b297d23a9d406ef056b" + integrity sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-numeric-separator" "^7.10.4" @@ -423,10 +492,10 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.1.tgz#cce122203fc8a32794296fc377c6dedaf4363797" - integrity sha512-c2uRpY6WzaVDzynVY9liyykS+kVU+WRZPMPYpkelXH8KBt1oXoI89kPbZKKG/jDT5UK92FTW2fZkZaJhdiBabw== +"@babel/plugin-proposal-optional-chaining@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz#e02f0ea1b5dc59d401ec16fb824679f683d3303c" + integrity sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" @@ -569,13 +638,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.12.1": +"@babel/plugin-transform-block-scoping@^7.0.0": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz#f0ee727874b42a208a48a586b84c3d222c2bbef1" integrity sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w== dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-transform-block-scoping@^7.12.11": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.12.tgz#d93a567a152c22aea3b1929bb118d1d0a175cdca" + integrity sha512-VOEPQ/ExOVqbukuP7BYJtI5ZxxsmegTwzZ04j1aF0dkSypGo9XpDHuOrABsJu+ie+penpSJheDJ11x1BEZNiyQ== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-classes@^7.0.0", "@babel/plugin-transform-classes@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz#65e650fcaddd3d88ddce67c0f834a3d436a32db6" @@ -745,30 +821,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-react-jsx-development@^7.12.5": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.5.tgz#677de5b96da310430d6cfb7fee16a1603afa3d56" - integrity sha512-1JJusg3iPgsZDthyWiCr3KQiGs31ikU/mSf2N2dSYEAO0GEImmVUbWf0VoSDGDFTAn5Dj4DUiR6SdIXHY7tELA== +"@babel/plugin-transform-react-jsx-development@^7.12.7": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.12.tgz#bccca33108fe99d95d7f9e82046bfe762e71f4e7" + integrity sha512-i1AxnKxHeMxUaWVXQOSIco4tvVvvCxMSfeBMnMM06mpaJt3g+MpxYQQrDfojUQldP1xxraPSJYSMEljoWM/dCg== dependencies: - "@babel/helper-builder-react-jsx-experimental" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/plugin-syntax-jsx" "^7.12.1" - -"@babel/plugin-transform-react-jsx-self@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.12.1.tgz#ef43cbca2a14f1bd17807dbe4376ff89d714cf28" - integrity sha512-FbpL0ieNWiiBB5tCldX17EtXgmzeEZjFrix72rQYeq9X6nUK38HCaxexzVQrZWXanxKJPKVVIU37gFjEQYkPkA== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-transform-react-jsx" "^7.12.12" -"@babel/plugin-transform-react-jsx-source@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.12.1.tgz#d07de6863f468da0809edcf79a1aa8ce2a82a26b" - integrity sha512-keQ5kBfjJNRc6zZN1/nVHCd6LLIHq4aUKcVnvE/2l+ZZROSbqoiGFRtT5t3Is89XJxBQaP7NLZX2jgGHdZvvFQ== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-transform-react-jsx@^7.0.0", "@babel/plugin-transform-react-jsx@^7.12.5": +"@babel/plugin-transform-react-jsx@^7.0.0": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.5.tgz#39ede0e30159770561b6963be143e40af3bde00c" integrity sha512-2xkcPqqrYiOQgSlM/iwto1paPijjsDbUynN13tI6bosDz/jOW3CRzYguIE8wKX32h+msbBM22Dv5fwrFkUOZjQ== @@ -778,6 +838,17 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-syntax-jsx" "^7.12.1" +"@babel/plugin-transform-react-jsx@^7.12.10", "@babel/plugin-transform-react-jsx@^7.12.12": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.12.tgz#b0da51ffe5f34b9a900e9f1f5fb814f9e512d25e" + integrity sha512-JDWGuzGNWscYcq8oJVCtSE61a5+XAOos+V0HrxnDieUus4UMnBEosDnY1VJqU5iZ4pA04QY7l0+JvHL1hZEfsw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.12.10" + "@babel/helper-module-imports" "^7.12.5" + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-jsx" "^7.12.1" + "@babel/types" "^7.12.12" + "@babel/plugin-transform-react-pure-annotations@^7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.12.1.tgz#05d46f0ab4d1339ac59adf20a1462c91b37a1a42" @@ -815,13 +886,12 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" -"@babel/plugin-transform-sticky-regex@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.1.tgz#5c24cf50de396d30e99afc8d1c700e8bce0f5caf" - integrity sha512-CiUgKQ3AGVk7kveIaPEET1jNDhZZEl1RPMWdTBE1799bdz++SwqDHStmxfCtDfBhQgCl38YRiSnrMuUMZIWSUQ== +"@babel/plugin-transform-sticky-regex@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz#560224613ab23987453948ed21d0b0b193fa7fad" + integrity sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg== dependencies: "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-regex" "^7.10.4" "@babel/plugin-transform-template-literals@^7.0.0", "@babel/plugin-transform-template-literals@^7.12.1": version "7.12.1" @@ -830,10 +900,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-transform-typeof-symbol@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz#9ca6be343d42512fbc2e68236a82ae64bc7af78a" - integrity sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q== +"@babel/plugin-transform-typeof-symbol@^7.12.10": + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.10.tgz#de01c4c8f96580bd00f183072b0d0ecdcf0dec4b" + integrity sha512-JQ6H8Rnsogh//ijxspCjc21YPd3VLVoYtAwv3zQmqAt8YGYUtdo5usNhdl4b9/Vir2kPFZl6n1h0PfUz4hJhaA== dependencies: "@babel/helper-plugin-utils" "^7.10.4" @@ -853,15 +923,15 @@ "@babel/helper-plugin-utils" "^7.10.4" "@babel/preset-env@^7.8.4": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.1.tgz#9c7e5ca82a19efc865384bb4989148d2ee5d7ac2" - integrity sha512-H8kxXmtPaAGT7TyBvSSkoSTUK6RHh61So05SyEbpmr0MCZrsNYn7mGMzzeYoOUCdHzww61k8XBft2TaES+xPLg== + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.11.tgz#55d5f7981487365c93dbbc84507b1c7215e857f9" + integrity sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw== dependencies: - "@babel/compat-data" "^7.12.1" - "@babel/helper-compilation-targets" "^7.12.1" - "@babel/helper-module-imports" "^7.12.1" + "@babel/compat-data" "^7.12.7" + "@babel/helper-compilation-targets" "^7.12.5" + "@babel/helper-module-imports" "^7.12.5" "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-validator-option" "^7.12.1" + "@babel/helper-validator-option" "^7.12.11" "@babel/plugin-proposal-async-generator-functions" "^7.12.1" "@babel/plugin-proposal-class-properties" "^7.12.1" "@babel/plugin-proposal-dynamic-import" "^7.12.1" @@ -869,10 +939,10 @@ "@babel/plugin-proposal-json-strings" "^7.12.1" "@babel/plugin-proposal-logical-assignment-operators" "^7.12.1" "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1" - "@babel/plugin-proposal-numeric-separator" "^7.12.1" + "@babel/plugin-proposal-numeric-separator" "^7.12.7" "@babel/plugin-proposal-object-rest-spread" "^7.12.1" "@babel/plugin-proposal-optional-catch-binding" "^7.12.1" - "@babel/plugin-proposal-optional-chaining" "^7.12.1" + "@babel/plugin-proposal-optional-chaining" "^7.12.7" "@babel/plugin-proposal-private-methods" "^7.12.1" "@babel/plugin-proposal-unicode-property-regex" "^7.12.1" "@babel/plugin-syntax-async-generators" "^7.8.0" @@ -890,7 +960,7 @@ "@babel/plugin-transform-arrow-functions" "^7.12.1" "@babel/plugin-transform-async-to-generator" "^7.12.1" "@babel/plugin-transform-block-scoped-functions" "^7.12.1" - "@babel/plugin-transform-block-scoping" "^7.12.1" + "@babel/plugin-transform-block-scoping" "^7.12.11" "@babel/plugin-transform-classes" "^7.12.1" "@babel/plugin-transform-computed-properties" "^7.12.1" "@babel/plugin-transform-destructuring" "^7.12.1" @@ -914,14 +984,14 @@ "@babel/plugin-transform-reserved-words" "^7.12.1" "@babel/plugin-transform-shorthand-properties" "^7.12.1" "@babel/plugin-transform-spread" "^7.12.1" - "@babel/plugin-transform-sticky-regex" "^7.12.1" + "@babel/plugin-transform-sticky-regex" "^7.12.7" "@babel/plugin-transform-template-literals" "^7.12.1" - "@babel/plugin-transform-typeof-symbol" "^7.12.1" + "@babel/plugin-transform-typeof-symbol" "^7.12.10" "@babel/plugin-transform-unicode-escapes" "^7.12.1" "@babel/plugin-transform-unicode-regex" "^7.12.1" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.12.1" - core-js-compat "^3.6.2" + "@babel/types" "^7.12.11" + core-js-compat "^3.8.0" semver "^5.5.0" "@babel/preset-modules@^0.1.3": @@ -936,16 +1006,14 @@ esutils "^2.0.2" "@babel/preset-react@^7.8.3": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.12.5.tgz#d45625f65d53612078a43867c5c6750e78772c56" - integrity sha512-jcs++VPrgyFehkMezHtezS2BpnUlR7tQFAyesJn1vGTO9aTFZrgIQrA5YydlTwxbcjMwkFY6i04flCigRRr3GA== + version "7.12.10" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.12.10.tgz#4fed65f296cbb0f5fb09de6be8cddc85cc909be9" + integrity sha512-vtQNjaHRl4DUpp+t+g4wvTHsLQuye+n0H/wsXIZRn69oz/fvNC7gQ4IK73zGJBaxvHoxElDvnYCthMcT7uzFoQ== dependencies: "@babel/helper-plugin-utils" "^7.10.4" "@babel/plugin-transform-react-display-name" "^7.12.1" - "@babel/plugin-transform-react-jsx" "^7.12.5" - "@babel/plugin-transform-react-jsx-development" "^7.12.5" - "@babel/plugin-transform-react-jsx-self" "^7.12.1" - "@babel/plugin-transform-react-jsx-source" "^7.12.1" + "@babel/plugin-transform-react-jsx" "^7.12.10" + "@babel/plugin-transform-react-jsx-development" "^7.12.7" "@babel/plugin-transform-react-pure-annotations" "^7.12.1" "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.8.4": @@ -964,6 +1032,15 @@ "@babel/parser" "^7.10.4" "@babel/types" "^7.10.4" +"@babel/template@^7.12.7": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" + integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.12.7" + "@babel/types" "^7.12.7" + "@babel/traverse@7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.1.tgz#941395e0c5cc86d5d3e75caa095d3924526f0c1e" @@ -979,7 +1056,7 @@ globals "^11.1.0" lodash "^4.17.19" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.4": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.4": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.5.tgz#78a0c68c8e8a35e4cacfd31db8bb303d5606f095" integrity sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA== @@ -994,6 +1071,21 @@ globals "^11.1.0" lodash "^4.17.19" +"@babel/traverse@^7.10.4", "@babel/traverse@^7.12.10": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376" + integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w== + dependencies: + "@babel/code-frame" "^7.12.11" + "@babel/generator" "^7.12.11" + "@babel/helper-function-name" "^7.12.11" + "@babel/helper-split-export-declaration" "^7.12.11" + "@babel/parser" "^7.12.11" + "@babel/types" "^7.12.12" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.19" + "@babel/types@7.12.1": version "7.12.1" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.1.tgz#e109d9ab99a8de735be287ee3d6a9947a190c4ae" @@ -1003,7 +1095,7 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.5", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.11.0", "@babel/types@^7.12.1", "@babel/types@^7.12.5": version "7.12.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96" integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA== @@ -1012,6 +1104,15 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.7", "@babel/types@^7.4.4": + version "7.12.12" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299" + integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ== + dependencies: + "@babel/helper-validator-identifier" "^7.12.11" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@concordance/react@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@concordance/react/-/react-2.0.0.tgz#aef913f27474c53731f4fd79cc2f54897de90fde" @@ -1384,6 +1485,63 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= +"@rollup/plugin-babel@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.2.2.tgz#e5623a01dd8e37e004ba87f2de218c611727d9b2" + integrity sha512-MjmH7GvFT4TW8xFdIeFS3wqIX646y5tACdxkTO+khbHvS3ZcVJL6vkAHLw2wqPmkhwCfWHoNsp15VYNwW6JEJA== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@rollup/pluginutils" "^3.1.0" + +"@rollup/plugin-commonjs@^17.0.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-17.0.0.tgz#2ae2228354cf0fbba6cf9f06f30b2c66a974324c" + integrity sha512-/omBIJG1nHQc+bgkYDuLpb/V08QyutP9amOrJRUSlYJZP+b/68gM//D8sxJe3Yry2QnYIr3QjR3x4AlxJEN3GA== + dependencies: + "@rollup/pluginutils" "^3.1.0" + commondir "^1.0.1" + estree-walker "^2.0.1" + glob "^7.1.6" + is-reference "^1.2.1" + magic-string "^0.25.7" + resolve "^1.17.0" + +"@rollup/plugin-json@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3" + integrity sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw== + dependencies: + "@rollup/pluginutils" "^3.0.8" + +"@rollup/plugin-node-resolve@^11.0.1": + version "11.0.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.0.1.tgz#d3765eec4bccf960801439a999382aed2dca959b" + integrity sha512-ltlsj/4Bhwwhb+Nb5xCz/6vieuEj2/BAkkqVIKmZwC7pIdl8srmgmglE4S0jFlZa32K4qvdQ6NHdmpRKD/LwoQ== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + builtin-modules "^3.1.0" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.19.0" + +"@rollup/plugin-replace@^2.3.4": + version "2.3.4" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.3.4.tgz#7dd84c17755d62b509577f2db37eb524d7ca88ca" + integrity sha512-waBhMzyAtjCL1GwZes2jaE9MjuQ/DQF2BatH3fRivUF3z0JBFrU0U6iBNC/4WR+2rLKhaAhPWDNPYp4mI6RqdQ== + dependencies: + "@rollup/pluginutils" "^3.1.0" + magic-string "^0.25.7" + +"@rollup/pluginutils@^3.0.8", "@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -1445,6 +1603,16 @@ "@types/keygrip" "*" "@types/node" "*" +"@types/estree@*": + version "0.0.45" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884" + integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g== + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + "@types/express-serve-static-core@*": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084" @@ -1588,6 +1756,13 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" + "@types/serve-static@*": version "1.13.6" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1" @@ -1646,14 +1821,6 @@ resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== -JSONStream@^1.0.3: - version "1.3.5" - resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" - integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== - dependencies: - jsonparse "^1.2.0" - through ">=2.2.7 <3" - abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1679,26 +1846,17 @@ acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== -acorn-node@^1.2.0, acorn-node@^1.3.0, acorn-node@^1.5.2, acorn-node@^1.6.1: - version "1.8.2" - resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" - integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== - dependencies: - acorn "^7.0.0" - acorn-walk "^7.0.0" - xtend "^4.0.2" - -acorn-walk@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" - integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== - acorn-walk@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.0.tgz#56ae4c0f434a45fff4a125e7ea95fa9c98f67a16" integrity sha512-oZRad/3SMOI/pxbbmqyurIx7jHw1wZDcR9G44L8pUVFEomX/0dH89SrM1KaDXuv1NpzAXz6Op/Xu/Qd5XXzdEA== -acorn@^7.0.0, acorn@^7.4.0: +acorn@^5.7.3: + version "5.7.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" + integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== + +acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -1992,11 +2150,6 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" -array-filter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" - integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= - array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" @@ -2053,16 +2206,6 @@ asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -2075,14 +2218,6 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assert@^1.4.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -2185,13 +2320,6 @@ ava@3.14.0: write-file-atomic "^3.0.3" yargs "^16.2.0" -available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" - integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== - dependencies: - array-filter "^1.0.0" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -2247,11 +2375,6 @@ babel-preset-fbjs@^3.3.0: "@babel/plugin-transform-template-literals" "^7.0.0" babel-plugin-syntax-trailing-function-commas "^7.0.0-beta.0" -babelify@^10.0.0: - version "10.0.0" - resolved "https://registry.yarnpkg.com/babelify/-/babelify-10.0.0.tgz#fe73b1a22583f06680d8d072e25a1e0d1d1d7fb5" - integrity sha512-X40FaxyH7t3X+JFAKvb1H9wooWKLRCi8pg3m8poqtdZaIng+bjzp9RvKQCvRjF9isHiPkXspbbXT/zwXLtwgwg== - backo2@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" @@ -2262,7 +2385,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-js@^1.0.2, base64-js@^1.3.1: +base64-js@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== @@ -2306,16 +2429,6 @@ blueimp-md5@^2.10.0: resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.18.0.tgz#1152be1335f0c6b3911ed9e36db54f3e6ac52935" integrity sha512-vE52okJvzsVWhcgUHOv+69OG3Mdg151xyn41aVQN/5W5S+S43qZhxECtYLAEHMSFWX6Mv5IZrzj3T5+JqXfj5Q== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== - -bn.js@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" - integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== - boolbase@^1.0.0, boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -2362,146 +2475,7 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browser-pack@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.1.0.tgz#c34ba10d0b9ce162b5af227c7131c92c2ecd5774" - integrity sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA== - dependencies: - JSONStream "^1.0.3" - combine-source-map "~0.8.0" - defined "^1.0.0" - safe-buffer "^5.1.1" - through2 "^2.0.0" - umd "^3.0.0" - -browser-resolve@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-2.0.0.tgz#99b7304cb392f8d73dba741bb2d7da28c6d7842b" - integrity sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ== - dependencies: - resolve "^1.17.0" - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" - integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= - dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@~0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserify@^17.0.0: - version "17.0.0" - resolved "https://registry.yarnpkg.com/browserify/-/browserify-17.0.0.tgz#4c48fed6c02bfa2b51fd3b670fddb805723cdc22" - integrity sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w== - dependencies: - JSONStream "^1.0.3" - assert "^1.4.0" - browser-pack "^6.0.1" - browser-resolve "^2.0.0" - browserify-zlib "~0.2.0" - buffer "~5.2.1" - cached-path-relative "^1.0.0" - concat-stream "^1.6.0" - console-browserify "^1.1.0" - constants-browserify "~1.0.0" - crypto-browserify "^3.0.0" - defined "^1.0.0" - deps-sort "^2.0.1" - domain-browser "^1.2.0" - duplexer2 "~0.1.2" - events "^3.0.0" - glob "^7.1.0" - has "^1.0.0" - htmlescape "^1.1.0" - https-browserify "^1.0.0" - inherits "~2.0.1" - insert-module-globals "^7.2.1" - labeled-stream-splicer "^2.0.0" - mkdirp-classic "^0.5.2" - module-deps "^6.2.3" - os-browserify "~0.3.0" - parents "^1.0.1" - path-browserify "^1.0.0" - process "~0.11.0" - punycode "^1.3.2" - querystring-es3 "~0.2.0" - read-only-stream "^2.0.0" - readable-stream "^2.0.2" - resolve "^1.1.4" - shasum-object "^1.0.0" - shell-quote "^1.6.1" - stream-browserify "^3.0.0" - stream-http "^3.0.0" - string_decoder "^1.1.1" - subarg "^1.0.0" - syntax-error "^1.1.1" - through2 "^2.0.0" - timers-browserify "^1.0.1" - tty-browserify "0.0.1" - url "~0.11.0" - util "~0.12.0" - vm-browserify "^1.0.0" - xtend "^4.0.0" - -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.14.6: +browserslist@^4.0.0, browserslist@^4.12.0: version "4.14.6" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.6.tgz#97702a9c212e0c6b6afefad913d3a1538e348457" integrity sha512-zeFYcUo85ENhc/zxHbiIp0LGzzTrE2Pv2JhxvS7kpUb9Q9D38kUX6Bie7pGutJ/5iF5rOxE7CepAuWD56xJ33A== @@ -2511,6 +2485,17 @@ browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4 escalade "^3.1.1" node-releases "^1.1.65" +browserslist@^4.14.5, browserslist@^4.15.0: + version "4.16.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.0.tgz#410277627500be3cb28a1bfe037586fbedf9488b" + integrity sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ== + dependencies: + caniuse-lite "^1.0.30001165" + colorette "^1.2.1" + electron-to-chromium "^1.3.621" + escalade "^3.1.1" + node-releases "^1.1.67" + bser@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" @@ -2528,16 +2513,16 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= +buffer-es6@^4.9.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/buffer-es6/-/buffer-es6-4.9.3.tgz#f26347b82df76fd37e18bcb5288c4970cfd5c404" + integrity sha1-8mNHuC33b9N+GLy1KIxJcM/VxAQ= + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - buffer@^5.5.0, buffer@^5.7.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -2546,14 +2531,6 @@ buffer@^5.5.0, buffer@^5.7.0: base64-js "^1.3.1" ieee754 "^1.1.13" -buffer@~5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6" - integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - bufferutil@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.2.tgz#79f68631910f6b993d870fc77dc0a2894eb96cd5" @@ -2561,10 +2538,10 @@ bufferutil@^4.0.1: dependencies: node-gyp-build "^4.2.0" -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +builtin-modules@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" + integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== busboy@^0.3.1: version "0.3.1" @@ -2591,11 +2568,6 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -cached-path-relative@^1.0.0, cached-path-relative@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.2.tgz#a13df4196d26776220cc3356eb147a52dba2c6db" - integrity sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg== - caching-transform@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-4.0.0.tgz#00d297a4206d71e2163c39eaffa8157ac0651f0f" @@ -2671,6 +2643,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001154: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001156.tgz#75c20937b6012fe2b02ab58b30d475bf0718de97" integrity sha512-z7qztybA2eFZTB6Z3yvaQBIoJpQtsewRD74adw2UbRWwsRq3jIPvgrQGawBMbfafekQaD21FWuXNcywtTDGGCw== +caniuse-lite@^1.0.30001165: + version "1.0.30001170" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001170.tgz#0088bfecc6a14694969e391cc29d7eb6362ca6a7" + integrity sha512-Dd4d/+0tsK0UNLrZs3CvNukqalnVTRrxb5mcQm8rHL49t7V5ZaTygwXkrq+FB+dVDf++4ri8eJnFEJAB8332PA== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -2736,14 +2713,6 @@ ci-parallel-vars@^1.0.1: resolved "https://registry.yarnpkg.com/ci-parallel-vars/-/ci-parallel-vars-1.0.1.tgz#e87ff0625ccf9d286985b29b4ada8485ca9ffbc2" integrity sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg== -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - classnames@^2.2.6: version "2.2.6" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" @@ -2875,16 +2844,6 @@ colorette@^1.2.1: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== -combine-source-map@^0.8.0, combine-source-map@~0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.8.0.tgz#a58d0df042c186fcf822a8e8015f5450d2d79a8b" - integrity sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos= - dependencies: - convert-source-map "~1.1.0" - inline-source-map "~0.6.0" - lodash.memoize "~3.0.3" - source-map "~0.5.3" - combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2917,16 +2876,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@~1.6.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - concordance@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/concordance/-/concordance-5.0.1.tgz#7a248aca8b286125d1d76f77b03320acf3f4ac63" @@ -2953,16 +2902,6 @@ configstore@^5.0.1: write-file-atomic "^3.0.0" xdg-basedir "^4.0.0" -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -constants-browserify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" @@ -2980,11 +2919,6 @@ convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" -convert-source-map@~1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" - integrity sha1-SCnId+n+SbMWHzvzZziI4gRpmGA= - convert-to-spaces@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz#7e3e48bbe6d997b1417ddca2868204b4d3d85715" @@ -3002,12 +2936,12 @@ copy-to-clipboard@^3.2.0: dependencies: toggle-selection "^1.0.6" -core-js-compat@^3.6.2: - version "3.7.0" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.7.0.tgz#8479c5d3d672d83f1f5ab94cf353e57113e065ed" - integrity sha512-V8yBI3+ZLDVomoWICO6kq/CD28Y4r1M7CWeO4AGpMdMfseu8bkSubBmUPySMGKRTS+su4XQ07zUkAsiu9FCWTg== +core-js-compat@^3.8.0: + version "3.8.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.8.1.tgz#8d1ddd341d660ba6194cbe0ce60f4c794c87a36e" + integrity sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ== dependencies: - browserslist "^4.14.6" + browserslist "^4.15.0" semver "7.0.0" core-js@^2.4.1: @@ -3057,37 +2991,6 @@ coveralls@^3.1.0: minimist "^1.2.5" request "^2.88.2" -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - cron-parser@^2.7.3: version "2.17.0" resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.17.0.tgz#5707421a7e0a73ee74675d1c032a2f14123f2cf8" @@ -3112,23 +3015,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-browserify@^3.0.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" @@ -3296,11 +3182,6 @@ d@1, d@^1.0.1: es5-ext "^0.10.50" type "^1.0.1" -dash-ast@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dash-ast/-/dash-ast-1.0.0.tgz#12029ba5fb2f8aa6f0a861795b23c1b4b6c27d37" - integrity sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA== - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -3404,6 +3285,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + default-require-extensions@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96" @@ -3430,11 +3316,6 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" -defined@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" - integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= - del@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" @@ -3474,33 +3355,6 @@ deprecated-decorator@^0.1.6: resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" integrity sha1-AJZjF7ehL+kvPMgx91g68ym4bDc= -deps-sort@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.1.tgz#9dfdc876d2bcec3386b6829ac52162cda9fa208d" - integrity sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw== - dependencies: - JSONStream "^1.0.3" - shasum-object "^1.0.0" - subarg "^1.0.0" - through2 "^2.0.0" - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -detective@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" - integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== - dependencies: - acorn-node "^1.6.1" - defined "^1.0.0" - minimist "^1.1.1" - dicer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" @@ -3508,15 +3362,6 @@ dicer@0.3.0: dependencies: streamsearch "0.1.2" -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -3554,11 +3399,6 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -domain-browser@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - domelementtype@1: version "1.3.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" @@ -3589,13 +3429,6 @@ dotenv@^8.2.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== -duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: - version "0.1.4" - resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" - integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= - dependencies: - readable-stream "^2.0.2" - duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -3614,18 +3447,10 @@ electron-to-chromium@^1.3.585: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.591.tgz#a18892bf1acb93f7b6e4da402705d564bc235017" integrity sha512-ol/0WzjL4NS4Kqy9VD6xXQON91xIihDT36sYCew/G/bnd1v0/4D+kahp26JauQhgFUjrdva3kRSo7URcUmQ+qw== -elliptic@^6.5.3: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" +electron-to-chromium@^1.3.621: + version "1.3.633" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz#16dd5aec9de03894e8d14a1db4cda8a369b9b7fe" + integrity sha512-bsVCsONiVX1abkWdH7KtpuDAhsQ3N3bjPYhROSAXE78roJKet0Y5wznA14JE9pzbwSZmSMAW6KiKYf1RvbTJkA== emittery@^0.7.2: version "0.7.2" @@ -3687,7 +3512,7 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.1.1" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5: +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: version "1.17.7" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== @@ -3972,6 +3797,26 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== +estree-walker@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39" + integrity sha512-XpCnW/AE10ws/kDAs37cngSkvgIR8aN3G0MS85m7dUpuK2EREo9VJ00uvw6Dg/hXEpfsE1I1TvJOJr+Z+TL+ig== + +estree-walker@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" + integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + esutils@^2.0.2, esutils@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -3982,19 +3827,6 @@ eventemitter3@^3.1.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== -events@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" - integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - ext@^1.1.2: version "1.4.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244" @@ -4054,11 +3886,6 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-safe-stringify@^2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" - integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== - fast-shallow-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" @@ -4193,11 +4020,6 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.0.tgz#a5d06b4a8b01e3a63771daa5cb7a1903e2e57067" integrity sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA== -foreach@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" - integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= - foreground-child@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53" @@ -4284,11 +4106,6 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-assigned-identifiers@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz#6dbf411de648cbaf8d9169ebb0d2d576191e2ff1" - integrity sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ== - get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -4341,7 +4158,7 @@ glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: dependencies: is-glob "^4.0.1" -glob@^7.1.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -4524,23 +4341,6 @@ has@^1.0.0, has@^1.0.3: dependencies: function-bind "^1.1.1" -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - hasha@^5.0.0: version "5.2.2" resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" @@ -4559,15 +4359,6 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - hoek@6.x.x: version "6.1.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" @@ -4610,11 +4401,6 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -htmlescape@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" - integrity sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E= - http-cache-semantics@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -4650,11 +4436,6 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -4703,7 +4484,7 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@^1.1.13, ieee754@^1.1.4: +ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -4802,16 +4583,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - inherits@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -4822,13 +4598,6 @@ ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== -inline-source-map@~0.6.0: - version "0.6.2" - resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5" - integrity sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU= - dependencies: - source-map "~0.5.3" - inline-style-prefixer@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz#d390957d26f281255fe101da863158ac6eb60911" @@ -4837,22 +4606,6 @@ inline-style-prefixer@^4.0.0: bowser "^1.7.3" css-in-js-utils "^2.0.0" -insert-module-globals@^7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.2.1.tgz#d5e33185181a4e1f33b15f7bf100ee91890d5cb3" - integrity sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg== - dependencies: - JSONStream "^1.0.3" - acorn-node "^1.5.2" - combine-source-map "^0.8.0" - concat-stream "^1.6.1" - is-buffer "^1.1.0" - path-is-absolute "^1.0.1" - process "~0.11.0" - through2 "^2.0.0" - undeclared-identifiers "^1.1.2" - xtend "^4.0.0" - internal-slot@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3" @@ -4872,11 +4625,6 @@ is-absolute-url@^2.0.0: resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4894,11 +4642,6 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-buffer@^1.1.0: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - is-callable@^1.1.4, is-callable@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" @@ -4930,6 +4673,13 @@ is-core-module@^2.0.0: dependencies: has "^1.0.3" +is-core-module@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" + integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== + dependencies: + has "^1.0.3" + is-date-object@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" @@ -4960,11 +4710,6 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-function@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" - integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== - is-glob@4.0.1, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" @@ -4985,6 +4730,11 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= + is-nan@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03" @@ -5037,6 +4787,13 @@ is-promise@4.0.0, is-promise@^4.0.0: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== +is-reference@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + is-regex@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" @@ -5078,16 +4835,6 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.1" -is-typed-array@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" - integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== - dependencies: - available-typed-arrays "^1.0.0" - es-abstract "^1.17.4" - foreach "^2.0.5" - has-symbols "^1.0.1" - is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -5207,6 +4954,15 @@ iterall@^1.1.3, iterall@^1.2.1: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + js-cookie@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" @@ -5303,11 +5059,6 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsonparse@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" - integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= - jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" @@ -5338,14 +5089,6 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" -labeled-stream-splicer@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz#42a41a16abcd46fd046306cf4f2c3576fffb1c21" - integrity sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw== - dependencies: - inherits "^2.0.1" - stream-splicer "^2.0.0" - latest-version@^5.0.0, latest-version@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" @@ -5447,11 +5190,6 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= -lodash.memoize@~3.0.3: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" - integrity sha1-LcvSwofLwKVcxCMovQxzYVDVPj8= - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -5525,6 +5263,20 @@ lru-cache@^5.0.0: dependencies: yallist "^3.0.2" +magic-string@^0.22.5: + version "0.22.5" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e" + integrity sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w== + dependencies: + vlq "^0.2.2" + +magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -5558,15 +5310,6 @@ md5-hex@^3.0.1: dependencies: blueimp-md5 "^2.10.0" -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - mdn-data@2.0.12: version "2.0.12" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.12.tgz#bbb658d08b38f574bbb88f7b83703defdcc46844" @@ -5590,6 +5333,11 @@ memory-pager@^1.0.2: resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -5620,14 +5368,6 @@ microrouter@^3.1.3: dependencies: url-pattern "^1.0.3" -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - mime-db@1.44.0: version "1.44.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" @@ -5655,16 +5395,6 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -5672,16 +5402,11 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mkdirp-classic@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -5704,27 +5429,6 @@ mocked-env@^1.3.2: lazy-ass "1.6.0" ramda "0.26.1" -module-deps@^6.2.3: - version "6.2.3" - resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.3.tgz#15490bc02af4b56cf62299c7c17cba32d71a96ee" - integrity sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA== - dependencies: - JSONStream "^1.0.3" - browser-resolve "^2.0.0" - cached-path-relative "^1.0.2" - concat-stream "~1.6.0" - defined "^1.0.0" - detective "^5.2.0" - duplexer2 "^0.1.2" - inherits "^2.0.1" - parents "^1.0.0" - readable-stream "^2.0.2" - resolve "^1.4.0" - stream-combiner2 "^1.1.1" - subarg "^1.0.0" - through2 "^2.0.0" - xtend "^4.0.0" - moment-timezone@^0.5.31: version "0.5.31" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" @@ -5920,6 +5624,11 @@ node-releases@^1.1.65: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.66.tgz#609bd0dc069381015cd982300bae51ab4f1b1814" integrity sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg== +node-releases@^1.1.67: + version "1.1.67" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" + integrity sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg== + node-schedule@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-1.3.2.tgz#d774b383e2a6f6ade59eecc62254aea07cd758cb" @@ -6173,11 +5882,6 @@ ora@^5.1.0: strip-ansi "^6.0.0" wcwidth "^1.0.1" -os-browserify@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -6293,11 +5997,6 @@ package-json@^6.3.0: registry-url "^5.0.0" semver "^6.2.0" -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -6305,24 +6004,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parents@^1.0.0, parents@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" - integrity sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E= - dependencies: - path-platform "~0.11.15" - -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -6361,11 +6042,6 @@ pascal-case@^3.1.1: no-case "^3.0.3" tslib "^1.10.0" -path-browserify@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" - integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -6376,7 +6052,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: +path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= @@ -6391,11 +6067,6 @@ path-parse@^1.0.6: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== -path-platform@~0.11.15: - version "0.11.15" - resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" - integrity sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I= - path-type@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" @@ -6408,17 +6079,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -6800,6 +6460,11 @@ pretty-ms@^7.0.1: dependencies: parse-ms "^2.1.0" +process-es6@^0.11.6: + version "0.11.6" + resolved "https://registry.yarnpkg.com/process-es6/-/process-es6-0.11.6.tgz#c6bb389f9a951f82bd4eb169600105bd2ff9c778" + integrity sha1-xrs4n5qVH4K9TrFpYAEFvS/5x3g= + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -6812,11 +6477,6 @@ process-on-spawn@^1.0.0: dependencies: fromentries "^1.2.0" -process@~0.11.0: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" @@ -6848,18 +6508,6 @@ pstree.remy@^1.1.7: resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -6868,16 +6516,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= - -punycode@^1.3.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -6900,36 +6538,18 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -querystring-es3@~0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - -querystring@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" - integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= - ramda@0.26.1: version "0.26.1" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06" integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: +randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - raw-body@2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" @@ -7027,13 +6647,6 @@ react@^17.0.1: loose-envify "^1.1.0" object-assign "^4.1.1" -read-only-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" - integrity sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A= - dependencies: - readable-stream "^2.0.2" - read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -7061,7 +6674,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.5, readable-stream@~2.3.6: +readable-stream@^2.3.5: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -7074,7 +6687,7 @@ readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.5, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -7313,7 +6926,7 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.1.4, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2, resolve@^1.4.0: +resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.3.2: version "1.18.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130" integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA== @@ -7321,6 +6934,14 @@ resolve@^1.1.4, resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18 is-core-module "^2.0.0" path-parse "^1.0.6" +resolve@^1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -7363,26 +6984,58 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== +rollup-plugin-node-globals@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-globals/-/rollup-plugin-node-globals-1.4.0.tgz#5e1f24a9bb97c0ef51249f625e16c7e61b7c020b" + integrity sha512-xRkB+W/m1KLIzPUmG0ofvR+CPNcvuCuNdjVBVS7ALKSxr3EDhnzNceGkGi1m8MToSli13AzKFYH4ie9w3I5L3g== dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" + acorn "^5.7.3" + buffer-es6 "^4.9.3" + estree-walker "^0.5.2" + magic-string "^0.22.5" + process-es6 "^0.11.6" + rollup-pluginutils "^2.3.1" + +rollup-plugin-terser@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" -rosid-handler-js@^13.0.2: - version "13.0.2" - resolved "https://registry.yarnpkg.com/rosid-handler-js/-/rosid-handler-js-13.0.2.tgz#85cdaab8688248f1b637f3ac7f3cb5dc34673ba9" - integrity sha512-oGcyJvwyfeqclFPgexLfKGKCwOnvY0P5h5/muzYRw3HaoPsm+F+igyhcFDsPYfl6EV9ILy1+BQCvsN0rstDjqw== +rollup-pluginutils@^2.3.1: + version "2.8.2" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" + integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== + dependencies: + estree-walker "^0.6.1" + +rollup@^2.35.1: + version "2.35.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.35.1.tgz#e6bc8d10893556a638066f89e8c97f422d03968c" + integrity sha512-q5KxEyWpprAIcainhVy6HfRttD9kutQpHbeqDTWnqAFNJotiojetK6uqmcydNMymBEtC4I8bCYR+J3mTMqeaUA== + optionalDependencies: + fsevents "~2.1.2" + +rosid-handler-js-next@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rosid-handler-js-next/-/rosid-handler-js-next-1.0.0.tgz#726a6f39e11210890dba35f7b85c70ef35ff2ad4" + integrity sha512-HY7+osXSSo9flgUvqi2u/c/iyRbzF/cgOr8ZMtOZQKp4qRpriDPgxUZqiJmoiH7BdYIkPmxZsu3BPGaDETdWbg== dependencies: "@babel/core" "^7.8.4" "@babel/preset-env" "^7.8.4" "@babel/preset-react" "^7.8.3" - babelify "^10.0.0" - browserify "^17.0.0" - loose-envify "^1.4.0" - terser "^5.3.5" + "@rollup/plugin-babel" "^5.2.2" + "@rollup/plugin-commonjs" "^17.0.0" + "@rollup/plugin-json" "^4.1.0" + "@rollup/plugin-node-resolve" "^11.0.1" + "@rollup/plugin-replace" "^2.3.4" + rollup "^2.35.1" + rollup-plugin-node-globals "^1.4.0" + rollup-plugin-terser "^7.0.2" rosid-handler-sass@^8.0.0: version "8.0.0" @@ -7421,7 +7074,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -7512,6 +7165,13 @@ serialize-error@^2.1.0: resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" integrity sha1-ULZ51WNc34Rme9yOWa9OW4HV9go= +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -7537,7 +7197,7 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: +sha.js@^2.4.11: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== @@ -7545,13 +7205,6 @@ sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shasum-object@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shasum-object/-/shasum-object-1.0.0.tgz#0b7b74ff5b66ecf9035475522fa05090ac47e29e" - integrity sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg== - dependencies: - fast-safe-stringify "^2.0.7" - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -7564,11 +7217,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== - shortid@^2.2.16: version "2.2.16" resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.16.tgz#b742b8f0cb96406fd391c76bfc18a67a57fe5608" @@ -7608,11 +7256,6 @@ signedsource@^1.0.0: resolved "https://registry.yarnpkg.com/signedsource/-/signedsource-1.0.0.tgz#1ddace4981798f93bd833973803d80d52e93ad6a" integrity sha1-HdrOSYF5j5O9gzlzgD2A1S6TrWo= -simple-concat@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" - integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== - simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -7666,7 +7309,7 @@ source-map@0.5.6: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= -source-map@^0.5.0, source-map@~0.5.3: +source-map@^0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -7681,7 +7324,7 @@ source-map@~0.7.2: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== -sourcemap-codec@^1.4.1: +sourcemap-codec@^1.4.1, sourcemap-codec@^1.4.4: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== @@ -7797,40 +7440,6 @@ stacktrace-js@^2.0.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -stream-browserify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" - integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== - dependencies: - inherits "~2.0.4" - readable-stream "^3.5.0" - -stream-combiner2@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" - integrity sha1-+02KFCDqNidk4hrUeAOXvry0HL4= - dependencies: - duplexer2 "~0.1.0" - readable-stream "^2.0.2" - -stream-http@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-3.1.1.tgz#0370a8017cf8d050b9a8554afe608f043eaff564" - integrity sha512-S7OqaYu0EkFpgeGFb/NPOoPLxFko7TPqtEeFg5DXPB4v/KETHG0Ln6fRFrNezoelpaDKmycEmmZ81cC9DAwgYg== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.4" - readable-stream "^3.6.0" - xtend "^4.0.2" - -stream-splicer@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.1.tgz#0b13b7ee2b5ac7e0609a7463d83899589a363fcd" - integrity sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg== - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.2" - streamsearch@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" @@ -7951,13 +7560,6 @@ stylis@3.5.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1" integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw== -subarg@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" - integrity sha1-9izxdYHplrSPyWVpn1TAauJouNI= - dependencies: - minimist "^1.1.0" - subscriptions-transport-ws@0.9.18, subscriptions-transport-ws@^0.9.11: version "0.9.18" resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.18.tgz#bcf02320c911fbadb054f7f928e51c6041a37b97" @@ -7994,7 +7596,7 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -8038,13 +7640,6 @@ sync-fetch@0.3.0: buffer "^5.7.0" node-fetch "^2.6.1" -syntax-error@^1.1.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.4.0.tgz#2d9d4ff5c064acb711594a3e3b95054ad51d907c" - integrity sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w== - dependencies: - acorn-node "^1.2.0" - table@^6.0.4: version "6.0.4" resolved "https://registry.yarnpkg.com/table/-/table-6.0.4.tgz#c523dd182177e926c723eb20e1b341238188aa0d" @@ -8076,10 +7671,10 @@ term-size@^2.1.0: resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== -terser@^5.3.5: - version "5.3.8" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.8.tgz#991ae8ba21a3d990579b54aa9af11586197a75dd" - integrity sha512-zVotuHoIfnYjtlurOouTazciEfL7V38QMAOhGqpXDEg6yT13cF4+fEP9b0rrCEQTn+tT46uxgFsTZzhygk+CzQ== +terser@^5.0.0: + version "5.5.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289" + integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ== dependencies: commander "^2.20.0" source-map "~0.7.2" @@ -8109,31 +7704,11 @@ throttle-debounce@^2.1.0: resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.3.0.tgz#fd31865e66502071e411817e241465b3e9c372e2" integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ== -through2@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -"through@>=2.2.7 <3": - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - time-zone@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/time-zone/-/time-zone-1.0.0.tgz#99c5bf55958966af6d06d83bdf3800dc82faec5d" integrity sha1-mcW/VZWJZq9tBtg73zgA3IL67F0= -timers-browserify@^1.0.1: - version "1.4.2" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" - integrity sha1-ycWLV1voQHN1y14kYtrO50NZ9B0= - dependencies: - process "~0.11.0" - timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" @@ -8232,11 +7807,6 @@ tslib@^2.0.0, tslib@~2.0.1, tslib@~2.0.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== -tty-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" - integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== - tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -8288,32 +7858,11 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - ua-parser-js@^0.7.18: version "0.7.22" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.22.tgz#960df60a5f911ea8f1c818f3747b99c6e177eae3" integrity sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q== -umd@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf" - integrity sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow== - -undeclared-identifiers@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz#9254c1d37bdac0ac2b52de4b6722792d2a91e30f" - integrity sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw== - dependencies: - acorn-node "^1.3.0" - dash-ast "^1.0.0" - get-assigned-identifiers "^1.2.0" - simple-concat "^1.0.0" - xtend "^4.0.1" - undefsafe@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" @@ -8446,14 +7995,6 @@ url-pattern@^1.0.3: resolved "https://registry.yarnpkg.com/url-pattern/-/url-pattern-1.0.3.tgz#0409292471b24f23c50d65a47931793d2b5acfc1" integrity sha1-BAkpJHGyTyPFDWWkeTF5PStaz8E= -url@~0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" - integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= - dependencies: - punycode "1.3.2" - querystring "0.2.0" - utf-8-validate@^5.0.2: version "5.0.3" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.3.tgz#3b64e418ad2ff829809025fdfef595eab2f03a27" @@ -8481,25 +8022,6 @@ util.promisify@^1.0.0, util.promisify@~1.0.0: has-symbols "^1.0.1" object.getownpropertydescriptors "^2.1.0" -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@~0.12.0: - version "0.12.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.3.tgz#971bb0292d2cc0c892dab7c6a5d37c2bec707888" - integrity sha512-I8XkoQwE+fPQEhy9v012V+TSdH2kp9ts29i20TaaDUXsg7x/onePbhFJUExBfv/2ay1ZOp/Vsm3nDlmnFGSAog== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - safe-buffer "^5.1.2" - which-typed-array "^1.1.2" - uuid@8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" @@ -8552,10 +8074,10 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vm-browserify@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vlq@^0.2.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" + integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== vue-template-compiler@^2.6.12: version "2.6.12" @@ -8604,18 +8126,6 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= -which-typed-array@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" - integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== - dependencies: - available-typed-arrays "^1.0.2" - es-abstract "^1.17.5" - foreach "^2.0.5" - function-bind "^1.1.1" - has-symbols "^1.0.1" - is-typed-array "^1.1.3" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -8695,11 +8205,6 @@ xss@^1.0.6: commander "^2.20.3" cssfilter "0.0.10" -xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" From ea67c813ad62b30902506a5265521ad13c2269d1 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Fri, 25 Dec 2020 16:12:42 +0100 Subject: [PATCH 114/208] Fix circular dependency --- src/ui/scripts/actions/index.js | 8 +------- src/ui/scripts/actions/token.js | 7 +++++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/ui/scripts/actions/index.js b/src/ui/scripts/actions/index.js index 591f7209..dd78ec87 100644 --- a/src/ui/scripts/actions/index.js +++ b/src/ui/scripts/actions/index.js @@ -5,10 +5,4 @@ export * from './route' export * from './filter' export * from './domains' export * from './events' -export * from './widgets' - -export const RESET_STATE = Symbol() - -export const resetState = () => ({ - type: RESET_STATE -}) \ No newline at end of file +export * from './widgets' \ No newline at end of file diff --git a/src/ui/scripts/actions/token.js b/src/ui/scripts/actions/token.js index 38b2652a..ec39c415 100644 --- a/src/ui/scripts/actions/token.js +++ b/src/ui/scripts/actions/token.js @@ -1,13 +1,16 @@ import api from '../utils/api' import signalHandler from '../utils/signalHandler' -import { resetState } from './index' - +export const RESET_STATE = Symbol() export const SET_TOKEN_START = Symbol() export const SET_TOKEN_END = Symbol() export const SET_TOKEN_FETCHING = Symbol() export const SET_TOKEN_ERROR = Symbol() +export const resetState = () => ({ + type: RESET_STATE +}) + export const setTokenStart = () => ({ type: SET_TOKEN_START }) From 949d6500f6f3fadc67e65a4d1a6e3c7fe7fa3fa0 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Fri, 25 Dec 2020 16:17:45 +0100 Subject: [PATCH 115/208] Only add nodeGlobals in development mode --- build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.js b/build.js index 65cd178a..6c55e2f5 100644 --- a/build.js +++ b/build.js @@ -49,7 +49,7 @@ const scripts = async () => { 'process.env.ACKEE_TRACKER': JSON.stringify(process.env.ACKEE_TRACKER), 'process.env.ACKEE_DEMO': JSON.stringify(isDemoMode === true ? 'true' : 'false') }, - nodeGlobals: true, + nodeGlobals: isDevelopmentMode === true, babel: false }) From 82ae82374d81bbf2c9bbc5df6469a437427fc54b Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 26 Dec 2020 11:52:29 +0100 Subject: [PATCH 116/208] Allow null for action values to reset an action value --- src/types/actions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/types/actions.js b/src/types/actions.js index 0e5f057d..82c76030 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -43,7 +43,7 @@ module.exports = gql` Numerical value that is added to all other numerical values of the key, grouped by day, month or year. Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. """ - value: PositiveFloat! + value: PositiveFloat """ Details allow you to store more data along with the associated action. """ @@ -69,8 +69,9 @@ module.exports = gql` """ Numerical value that is added to all other numerical values of the key, grouped by day, month or year. Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. + Reset an existing value using 'null'. """ - value: PositiveFloat! + value: PositiveFloat """ Details allow you to store more data along with the associated action. """ From c5ddcba058ccc1e5b9f5cc839cae299028c9711b Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 26 Dec 2020 16:38:47 +0100 Subject: [PATCH 117/208] Action key is now required --- src/types/actions.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types/actions.js b/src/types/actions.js index 82c76030..d3249dd8 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -14,7 +14,7 @@ module.exports = gql` """ Optional key that will be used to group similar actions in the UI. """ - key: String + key: String! """ Numerical value that is added to all other numerical values of the key, grouped by day, month or year. Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. @@ -36,9 +36,9 @@ module.exports = gql` input CreateActionInput { """ - Optional key that will be used to group similar actions in the UI. + Key that will be used to group similar actions in the UI. """ - key: String + key: String! """ Numerical value that is added to all other numerical values of the key, grouped by day, month or year. Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. @@ -63,9 +63,9 @@ module.exports = gql` input UpdateActionInput { """ - Optional key that will be used to group similar actions in the UI. + Key that will be used to group similar actions in the UI. """ - key: String + key: String! """ Numerical value that is added to all other numerical values of the key, grouped by day, month or year. Use '1' to count how many times an event occurred or a price (e.g. '1.99') to see the sum of successful checkouts in a shop. From a5cab2a795916e419a47c80988cc0f148b91f870 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 26 Dec 2020 16:46:45 +0100 Subject: [PATCH 118/208] Usage example for events --- src/ui/scripts/components/modals/ModalEventEdit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/scripts/components/modals/ModalEventEdit.js b/src/ui/scripts/components/modals/ModalEventEdit.js index 4d0014a7..d784c7e6 100644 --- a/src/ui/scripts/components/modals/ModalEventEdit.js +++ b/src/ui/scripts/components/modals/ModalEventEdit.js @@ -98,13 +98,13 @@ const ModalEventEdit = (props) => { copyOnFocus: true }), - h(Label, { htmlFor: embedId }, 'Embed code'), + h(Label, { htmlFor: embedId }, 'Usage example'), h(Textarea, { id: embedId, readOnly: true, rows: 4, - value: `ackeeTracker.create`, + value: `ackeeTracker.action('${ props.id }', { key: 'Click', value: '1' })`, copyOnFocus: true }) @@ -136,7 +136,7 @@ const ModalEventEdit = (props) => { h('button', { className: 'card__button card__button--primary link color-white', disabled: props.fetching === true || props.active === false - }, props.fetching === true ? h(Spinner) : 'Edit') + }, props.fetching === true ? h(Spinner) : 'Save') ) ) From 175a1e399f9306fca0899e20616f1e8c4b24680a Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sat, 26 Dec 2020 19:02:55 +0100 Subject: [PATCH 119/208] Remove `ignoreOwnVisits` from docs --- docs/CORS headers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CORS headers.md b/docs/CORS headers.md index 5d1b9067..6fd12875 100644 --- a/docs/CORS headers.md +++ b/docs/CORS headers.md @@ -50,9 +50,9 @@ Access-Control-Allow-Headers: Content-Type ### Credentials -The `Access-Control-Allow-Credentials` header tells the browser to include the `ackee_ignore` cookie in requests even when you're on a different (sub-)domain. This allows Ackee to ignore your own visits when the [`ignoreOwnVisits` option in ackee-tracker](https://github.com/electerious/ackee-tracker#-options) is enabled and when your browser doesn't block third-party cookies. +The `Access-Control-Allow-Credentials` header tells the browser to include the `ackee_ignore` cookie in requests even when you're on a different (sub-)domain. This allows Ackee to ignore your own visits. -> ℹ️ Some browsers strictly block third-party cookies when Ackee runs on a different domain than the site you're visiting. Therefore, it may happen that your own visits still find their way into your statistics, even when the option `ignoreOwnVisits` is turned on. +> ℹ️ Some browsers strictly block third-party cookies when Ackee runs on a different domain than the site you're visiting. Therefore, it may happen that your own visits still find their way into your statistics. ``` Access-Control-Allow-Credentials: true From 012c19f4b81e69178d7a0130c810bd904f5304d2 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 11:51:08 +0100 Subject: [PATCH 120/208] Fix recent events --- src/aggregations/aggregateRecentActions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/aggregations/aggregateRecentActions.js b/src/aggregations/aggregateRecentActions.js index acd18f3a..d9ca3670 100644 --- a/src/aggregations/aggregateRecentActions.js +++ b/src/aggregations/aggregateRecentActions.js @@ -13,7 +13,9 @@ module.exports = (ids, limit) => { }, { $project: { - _id: {}, + _id: { + key: '$key' + }, created: '$created' } }, @@ -23,7 +25,6 @@ module.exports = (ids, limit) => { ] aggregation[0].$match.key = { $ne: null } - aggregation[2].$group._id.key = '$key' return aggregation From 884e6280d9605af9c184d51eee79e0e81fc0d31c Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 11:51:30 +0100 Subject: [PATCH 121/208] Added event examples to API --- docs/API.md | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/docs/API.md b/docs/API.md index da94ca61..07f0bcf8 100644 --- a/docs/API.md +++ b/docs/API.md @@ -96,7 +96,13 @@ The time zone of the server will be used as a fallback. ## Queries -Queries are used to receive data. Here are a few examples: +Queries are used to receive data. Here are a few examples. + +- [Get all domains](#Get%20all%20domains) +- [Get a specific domain](#Get%20a%20specific%20domain) +- [Get facts of domains](#Get%20facts%20of%20domains) +- [Get statistics of domains](#Get%20statistics%20of%20domains) +- [Get events](#Get%20events) ### Get all domains @@ -197,9 +203,37 @@ query getDomainsStatistics { } ``` +### Get events + +```graphql +query getEvents { + events { + id + title + statistics { + chart(interval: DAILY) { + id + count + } + list(sorting: TOP) { + id + count + } + } + } +} +``` + ## Mutations -Mutations are used to add, update or delete data. Here are a few examples: +Mutations are used to add, update or delete data. Here are a few examples. + +- [Create a domain](#Create%20a%20domain) +- [Delete a domain](#Delete%20a%20domain) +- [Create a record](#Create%20a%20record) +- [Create an event](#Create%20an%20event) +- [Create an action](#Create%20an%20action) +- [Update an action](#Update%20an%20action) ### Create a domain @@ -257,4 +291,70 @@ mutation createRecord($domainId: ID!, $input: CreateRecordInput!) { "siteLocation": "https://example.com" } } +``` + +### Create an event + +```graphql +mutation createEvent($input: CreateEventInput!) { + createEvent(input: $input) { + payload { + id + title + } + } +} +``` + +```json +{ + "input": { + "title": "Event Title", + "type": "CHART" + } +} +``` + +### Create an action + +```graphql +mutation createAction($eventId: ID!, $input: CreateActionInput!) { + createAction(eventId: $eventId, input: $input) { + payload { + id + } + } +} +``` + +```json +{ + "eventId": "c8865d94-9077-420f-86a0-32545bcbf61b", + "input": { + "key": "Action Key", + "value": 1 + } +} +``` + +### Update an action + +```graphql +mutation updateAction($id: ID!, $input: UpdateActionInput!) { + updateAction(id: $id, input: $input) { + payload { + id + } + } +} +``` + +```json +{ + "id": "34df5a09-498f-45c1-822c-6b1f80de5f8c", + "input": { + "key": "Action Key", + "value": null + } +} ``` \ No newline at end of file From b8bb0a774f14640c2079a0056c517dc415046791 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 12:12:48 +0100 Subject: [PATCH 122/208] Adjust syntax --- test/resolvers/domains.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/resolvers/domains.js b/test/resolvers/domains.js index 453c69b4..a7e1dd63 100644 --- a/test/resolvers/domains.js +++ b/test/resolvers/domains.js @@ -59,7 +59,7 @@ test.serial('update domain', async (t) => { updateDomain(id: $id, input: $input) { success payload { - id, + id title } } From b9422acbe5b9a8ab81c84b00bf98dec8924389fe Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 12:13:04 +0100 Subject: [PATCH 123/208] Add tests for events --- test/resolvers/_utils.js | 2 + test/resolvers/events.js | 163 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 test/resolvers/events.js diff --git a/test/resolvers/_utils.js b/test/resolvers/_utils.js index 8142ae2f..1a188cc3 100644 --- a/test/resolvers/_utils.js +++ b/test/resolvers/_utils.js @@ -6,6 +6,7 @@ const fetch = require('node-fetch') const Token = require('../../src/models/Token') const Domain = require('../../src/models/Domain') +const Event = require('../../src/models/Event') const Record = require('../../src/models/Record') const connect = require('../../src/utils/connect') const createArray = require('../../src/utils/createArray') @@ -22,6 +23,7 @@ const fillDatabase = async (t) => { // Saves to context so tests can access IDs t.context.token = await Token.create({}) t.context.domain = await Domain.create({ title: 'Example' }) + t.context.event = await Event.create({ title: 'Example', type: 'CHART' }) const now = Date.now() diff --git a/test/resolvers/events.js b/test/resolvers/events.js new file mode 100644 index 00000000..9d0b57d4 --- /dev/null +++ b/test/resolvers/events.js @@ -0,0 +1,163 @@ +'use strict' + +const test = require('ava') +const listen = require('test-listen') +const uuid = require('uuid').v4 + +const server = require('../../src/server') +const { connectToDatabase, fillDatabase, cleanupDatabase, disconnectFromDatabase, api } = require('./_utils') + +const base = listen(server) + +let validEvent = null + +const defaultTitle = uuid() +const defaultType = 'CHART' +const updatedTitle = uuid() +const updatedType = 'LIST' + +test.before(connectToDatabase) +test.beforeEach(fillDatabase) +test.afterEach.always(cleanupDatabase) +test.after.always(disconnectFromDatabase) + +test.serial('create event', async (t) => { + + const body = { + query: ` + mutation createEvent($input: CreateEventInput!) { + createEvent(input: $input) { + success, + payload { + id + title + type + } + } + } + `, + variables: { + input: { + title: defaultTitle, + type: defaultType + } + } + } + + const { json } = await api(base, body, t.context.token.id) + + t.true(json.data.createEvent.success) + t.is(typeof json.data.createEvent.payload.id, 'string') + t.is(json.data.createEvent.payload.title, defaultTitle) + t.is(json.data.createEvent.payload.type, defaultType) + + // Save event for the next test + validEvent = json.data.createEvent.payload + +}) + +test.serial('update event', async (t) => { + + const body = { + query: ` + mutation updateEvent($id: ID!, $input: UpdateEventInput!) { + updateEvent(id: $id, input: $input) { + success + payload { + id + title + type + } + } + } + `, + variables: { + id: validEvent.id, + input: { + title: updatedTitle, + type: updatedType + } + } + } + + const { json } = await api(base, body, t.context.token.id) + + t.true(json.data.updateEvent.success) + t.is(json.data.updateEvent.payload.id, validEvent.id) + t.is(json.data.updateEvent.payload.title, updatedTitle) + t.is(json.data.updateEvent.payload.type, updatedType) + + // Save event for the next test + validEvent = json.data.updateEvent.payload + +}) + +test.serial('fetch events', async (t) => { + + const body = { + query: ` + query fetchEvents { + events { + id + title + type + } + } + ` + } + + const { json } = await api(base, body, t.context.token.id) + + const events = json.data.events + const event = events.find((event) => event.id === validEvent.id) + + t.is(event.title, validEvent.title) + t.is(event.type, validEvent.type) + +}) + +test.serial('fetch event', async (t) => { + + const body = { + query: ` + query fetchEvent($id: ID!) { + event(id: $id) { + id + title + type + } + } + `, + variables: { + id: validEvent.id + } + } + + const { json } = await api(base, body, t.context.token.id) + + t.is(json.data.event.id, validEvent.id) + t.is(json.data.event.title, validEvent.title) + t.is(json.data.event.type, validEvent.type) + +}) + +test.serial('delete event', async (t) => { + + const body = { + query: ` + mutation deleteEvent($id: ID!) { + deleteEvent(id: $id) { + success + } + } + `, + variables: { + id: validEvent.id + } + } + + const { json } = await api(base, body, t.context.token.id) + + t.true(json.data.deleteEvent.success) + +}) \ No newline at end of file From 97d81b0e7e21a285f0e9c07f7d169ccb4682749e Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 12:13:39 +0100 Subject: [PATCH 124/208] Adjust syntax --- test/resolvers/records.js | 1 - test/resolvers/tokens.js | 1 - 2 files changed, 2 deletions(-) diff --git a/test/resolvers/records.js b/test/resolvers/records.js index a1bec74a..29ece379 100644 --- a/test/resolvers/records.js +++ b/test/resolvers/records.js @@ -18,7 +18,6 @@ test.after.always(disconnectFromDatabase) test.serial('create record', async (t) => { - const body = { query: ` mutation createRecord($domainId: ID!, $input: CreateRecordInput!) { diff --git a/test/resolvers/tokens.js b/test/resolvers/tokens.js index 5d0a9912..4b03a2f4 100644 --- a/test/resolvers/tokens.js +++ b/test/resolvers/tokens.js @@ -18,7 +18,6 @@ test.after.always(disconnectFromDatabase) test.serial('return token and cookie after successful login', async (t) => { - const username = 'admin' const password = '123456' From 1bff57538eb1a8e2c747c433817c96b9cc950a82 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 12:38:36 +0100 Subject: [PATCH 125/208] Ignore own actions --- src/resolvers/actions.js | 21 +++++++++++++++++++-- src/resolvers/records.js | 4 ++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/resolvers/actions.js b/src/resolvers/actions.js index 3788ff55..7f00e559 100644 --- a/src/resolvers/actions.js +++ b/src/resolvers/actions.js @@ -22,7 +22,17 @@ const polish = (obj) => { module.exports = { Mutation: { - createAction: async (parent, { eventId, input }) => { + createAction: async (parent, { eventId, input }, { isIgnored }) => { + + // Ignore your own actions when logged in + if (isIgnored === true) { + return { + success: true, + payload: { + id: '88888888-8888-8888-8888-888888888888' + } + } + } const data = polish({ ...input, eventId }) @@ -52,7 +62,14 @@ module.exports = { } }, - updateAction: async (parent, { id, input }) => { + updateAction: async (parent, { id, input }, { isIgnored }) => { + + // Ignore your own actions when logged in + if (isIgnored === true) { + return { + success: true + } + } let entry diff --git a/src/resolvers/records.js b/src/resolvers/records.js index 573d88b7..09927d2d 100644 --- a/src/resolvers/records.js +++ b/src/resolvers/records.js @@ -67,7 +67,7 @@ module.exports = { Mutation: { createRecord: async (parent, { domainId, input }, { ip, userAgent, isIgnored }) => { - // Ignore your own visit when logged in + // Ignore your own records when logged in if (isIgnored === true) { return { success: true, @@ -112,7 +112,7 @@ module.exports = { }, updateRecord: async (parent, { id }, { isIgnored }) => { - // Ignore your own visit when logged in + // Ignore your own records when logged in if (isIgnored === true) { return { success: true From 213492e8c7cd0c4f778a233076d7cdedaa925045 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 12:39:06 +0100 Subject: [PATCH 126/208] Action tests --- test/resolvers/actions.js | 144 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 test/resolvers/actions.js diff --git a/test/resolvers/actions.js b/test/resolvers/actions.js new file mode 100644 index 00000000..c1c916aa --- /dev/null +++ b/test/resolvers/actions.js @@ -0,0 +1,144 @@ +'use strict' + +const test = require('ava') +const listen = require('test-listen') +const uuid = require('uuid').v4 + +const server = require('../../src/server') +const { connectToDatabase, fillDatabase, cleanupDatabase, disconnectFromDatabase, api } = require('./_utils') + +const base = listen(server) + +let validAction = null +let ignoredAction = null + +const defaultKey = uuid() +const defaultValue = 1 +const updatedKey = uuid() +const updatedValue = null + +test.before(connectToDatabase) +test.beforeEach(fillDatabase) +test.afterEach.always(cleanupDatabase) +test.after.always(disconnectFromDatabase) + +test.serial('create action', async (t) => { + + + const body = { + query: ` + mutation createAction($eventId: ID!, $input: CreateActionInput!) { + createAction(eventId: $eventId, input: $input) { + success + payload { + id + key + value + } + } + } + `, + variables: { + eventId: t.context.event.id, + input: { + key: defaultKey, + value: defaultValue + } + } + } + + const { json } = await api(base, body, t.context.token.id) + + t.true(json.data.createAction.success) + t.is(typeof json.data.createAction.payload.id, 'string') + t.is(json.data.createAction.payload.key, defaultKey) + t.is(json.data.createAction.payload.value, defaultValue) + + // Save action for the next test + validAction = json.data.createAction.payload + +}) + +test.serial('update action', async (t) => { + + const body = { + query: ` + mutation updateAction($id: ID!, $input: UpdateActionInput!) { + updateAction(id: $id, input: $input) { + success + } + } + `, + variables: { + id: validAction.id, + input: { + key: updatedKey, + value: updatedValue + } + } + } + + const { json } = await api(base, body, t.context.token.id) + + t.true(json.data.updateAction.success) + +}) + +test.serial('ignore action creation when logged in', async (t) => { + + const body = { + query: ` + mutation createAction($eventId: ID!, $input: CreateActionInput!) { + createAction(eventId: $eventId, input: $input) { + success + payload { + id + } + } + } + `, + variables: { + eventId: t.context.event.id, + input: { + key: uuid() + } + } + } + + const { json } = await api(base, body, t.context.token.id, { + Cookie: 'ackee_ignore=1' + }) + + t.true(json.data.createAction.success) + t.is(json.data.createAction.payload.id, '88888888-8888-8888-8888-888888888888') + + // Save action for the next test + ignoredAction = json.data.createAction.payload + +}) + +test.serial('ignore action update when logged in', async (t) => { + + const body = { + query: ` + mutation updateAction($id: ID!, $input: UpdateActionInput!) { + updateAction(id: $id, input: $input) { + success + } + } + `, + variables: { + id: ignoredAction.id, + input: { + key: uuid() + } + } + } + + const { json } = await api(base, body, t.context.token.id, { + Cookie: 'ackee_ignore=1' + }) + + t.true(json.data.updateAction.success) + +}) \ No newline at end of file From c298cd9d24142d1dbf6eb04ee2b0ce42692891e3 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 13:33:26 +0100 Subject: [PATCH 127/208] Move key into aggregation instead of adding it later --- src/aggregations/aggregateNewActions.js | 5 +++-- src/aggregations/aggregateTopActions.js | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/aggregations/aggregateNewActions.js b/src/aggregations/aggregateNewActions.js index 66227049..8f8e8304 100644 --- a/src/aggregations/aggregateNewActions.js +++ b/src/aggregations/aggregateNewActions.js @@ -8,7 +8,9 @@ module.exports = (ids, limit) => { matchEvents(ids), { $group: { - _id: {}, + _id: { + key: '$key' + }, count: { $sum: '$value' }, @@ -28,7 +30,6 @@ module.exports = (ids, limit) => { ] aggregation[0].$match.key = { $ne: null } - aggregation[1].$group._id.key = '$key' return aggregation diff --git a/src/aggregations/aggregateTopActions.js b/src/aggregations/aggregateTopActions.js index d790a9be..bcf8a008 100644 --- a/src/aggregations/aggregateTopActions.js +++ b/src/aggregations/aggregateTopActions.js @@ -9,7 +9,9 @@ module.exports = (ids, range, limit, dateDetails) => { matchEvents(ids), { $group: { - _id: {}, + _id: { + key: '$key' + }, count: { $sum: '$value' } @@ -26,7 +28,6 @@ module.exports = (ids, range, limit, dateDetails) => { ] aggregation[0].$match.key = { $ne: null } - aggregation[1].$group._id.key = '$key' if (range === ranges.RANGES_LAST_24_HOURS) { aggregation[0].$match.created = { $gte: dateDetails.lastHours(24) } From 502c657bfdaf556f7a960fc4717b078229259f0d Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 13:34:06 +0100 Subject: [PATCH 128/208] Add tests for actions --- test/resolvers/_utils.js | 12 ++- .../_utils.js | 0 .../browsers.js | 0 .../devices.js | 0 .../durations.js | 0 .../languages.js | 0 .../{statistics => domainStatistics}/pages.js | 0 .../referrers.js | 0 .../{statistics => domainStatistics}/sizes.js | 0 .../systems.js | 0 .../{statistics => domainStatistics}/views.js | 9 +++ test/resolvers/eventStatistics/_utils.js | 30 +++++++ test/resolvers/eventStatistics/chart.js | 80 +++++++++++++++++++ test/resolvers/eventStatistics/list.js | 71 ++++++++++++++++ 14 files changed, 201 insertions(+), 1 deletion(-) rename test/resolvers/{statistics => domainStatistics}/_utils.js (100%) rename test/resolvers/{statistics => domainStatistics}/browsers.js (100%) rename test/resolvers/{statistics => domainStatistics}/devices.js (100%) rename test/resolvers/{statistics => domainStatistics}/durations.js (100%) rename test/resolvers/{statistics => domainStatistics}/languages.js (100%) rename test/resolvers/{statistics => domainStatistics}/pages.js (100%) rename test/resolvers/{statistics => domainStatistics}/referrers.js (100%) rename test/resolvers/{statistics => domainStatistics}/sizes.js (100%) rename test/resolvers/{statistics => domainStatistics}/systems.js (100%) rename test/resolvers/{statistics => domainStatistics}/views.js (94%) create mode 100644 test/resolvers/eventStatistics/_utils.js create mode 100644 test/resolvers/eventStatistics/chart.js create mode 100644 test/resolvers/eventStatistics/list.js diff --git a/test/resolvers/_utils.js b/test/resolvers/_utils.js index 1a188cc3..b0334f18 100644 --- a/test/resolvers/_utils.js +++ b/test/resolvers/_utils.js @@ -8,6 +8,7 @@ const Token = require('../../src/models/Token') const Domain = require('../../src/models/Domain') const Event = require('../../src/models/Event') const Record = require('../../src/models/Record') +const Action = require('../../src/models/Action') const connect = require('../../src/utils/connect') const createArray = require('../../src/utils/createArray') const { day, minute } = require('../../src/utils/times') @@ -44,12 +45,21 @@ const fillDatabase = async (t) => { browserVersion: i > 7 ? '13.0' : '14.0', browserWidth: 414, browserHeight: 719, - // Add fake minute visit per day + // Set fake duration created: now - i * day - minute, updated: now - i * day })) + const actions = createArray(14).map((item, i) => ({ + eventId: t.context.event.id, + key: `Key ${ i + 1 }`, + value: i + 1, + created: now - i * day, + updated: now - i * day + })) + await Record.insertMany(records) + await Action.insertMany(actions) } const cleanupDatabase = async (t) => { diff --git a/test/resolvers/statistics/_utils.js b/test/resolvers/domainStatistics/_utils.js similarity index 100% rename from test/resolvers/statistics/_utils.js rename to test/resolvers/domainStatistics/_utils.js diff --git a/test/resolvers/statistics/browsers.js b/test/resolvers/domainStatistics/browsers.js similarity index 100% rename from test/resolvers/statistics/browsers.js rename to test/resolvers/domainStatistics/browsers.js diff --git a/test/resolvers/statistics/devices.js b/test/resolvers/domainStatistics/devices.js similarity index 100% rename from test/resolvers/statistics/devices.js rename to test/resolvers/domainStatistics/devices.js diff --git a/test/resolvers/statistics/durations.js b/test/resolvers/domainStatistics/durations.js similarity index 100% rename from test/resolvers/statistics/durations.js rename to test/resolvers/domainStatistics/durations.js diff --git a/test/resolvers/statistics/languages.js b/test/resolvers/domainStatistics/languages.js similarity index 100% rename from test/resolvers/statistics/languages.js rename to test/resolvers/domainStatistics/languages.js diff --git a/test/resolvers/statistics/pages.js b/test/resolvers/domainStatistics/pages.js similarity index 100% rename from test/resolvers/statistics/pages.js rename to test/resolvers/domainStatistics/pages.js diff --git a/test/resolvers/statistics/referrers.js b/test/resolvers/domainStatistics/referrers.js similarity index 100% rename from test/resolvers/statistics/referrers.js rename to test/resolvers/domainStatistics/referrers.js diff --git a/test/resolvers/statistics/sizes.js b/test/resolvers/domainStatistics/sizes.js similarity index 100% rename from test/resolvers/statistics/sizes.js rename to test/resolvers/domainStatistics/sizes.js diff --git a/test/resolvers/statistics/systems.js b/test/resolvers/domainStatistics/systems.js similarity index 100% rename from test/resolvers/statistics/systems.js rename to test/resolvers/domainStatistics/systems.js diff --git a/test/resolvers/statistics/views.js b/test/resolvers/domainStatistics/views.js similarity index 94% rename from test/resolvers/statistics/views.js rename to test/resolvers/domainStatistics/views.js index abd1ef8f..93afdb31 100644 --- a/test/resolvers/statistics/views.js +++ b/test/resolvers/domainStatistics/views.js @@ -76,6 +76,15 @@ test(macro, { t.is(typeof views[0].count, 'number') }) +test(macro, { + interval: 'YEARLY', + type: 'UNIQUE', + limit: 1 +}, (t, views) => { + t.is(views.length, 1) + t.is(typeof views[0].count, 'number') +}) + test(macro, { interval: 'DAILY', type: 'TOTAL' diff --git a/test/resolvers/eventStatistics/_utils.js b/test/resolvers/eventStatistics/_utils.js new file mode 100644 index 00000000..75f2f118 --- /dev/null +++ b/test/resolvers/eventStatistics/_utils.js @@ -0,0 +1,30 @@ +'use strict' + +const { api } = require('../_utils') + +const getStats = async ({ base, token, eventId, fragment }) => { + + const body = { + query: ` + query fetchStatistics($id: ID!) { + event(id: $id) { + statistics { + ${ fragment } + } + } + } + `, + variables: { + id: eventId + } + } + + const { json } = await api(base, body, token) + + return json.data.event.statistics + +} + +module.exports = { + getStats +} \ No newline at end of file diff --git a/test/resolvers/eventStatistics/chart.js b/test/resolvers/eventStatistics/chart.js new file mode 100644 index 00000000..c294249c --- /dev/null +++ b/test/resolvers/eventStatistics/chart.js @@ -0,0 +1,80 @@ +'use strict' + +const test = require('ava') +const listen = require('test-listen') + +const server = require('../../../src/server') +const { connectToDatabase, fillDatabase, cleanupDatabase, disconnectFromDatabase } = require('../_utils') +const { getStats } = require('./_utils') + +const base = listen(server) + +test.before(connectToDatabase) +test.beforeEach(fillDatabase) +test.afterEach.always(cleanupDatabase) +test.after.always(disconnectFromDatabase) + +const macro = async (t, variables, assertions) => { + const limit = variables.limit == null ? '' : `, limit: ${ variables.limit }` + + const statistics = await getStats({ + base, + token: t.context.token.id, + eventId: t.context.event.id, + fragment: ` + chart(interval: ${ variables.interval }${ limit }) { + id + count + } + ` + }) + + assertions(t, statistics.chart) +} + +macro.title = (providedTitle, opts) => `fetch ${ Object.values(opts).join(' and ') } chart entries` + +test(macro, { + interval: 'DAILY' +}, (t, entries) => { + t.is(entries.length, 14) + t.is(entries[0].count, 1) +}) + +test(macro, { + interval: 'DAILY', + limit: 1 +}, (t, entries) => { + t.is(entries.length, 1) + t.is(entries[0].count, 1) +}) + +test(macro, { + interval: 'MONTHLY' +}, (t, entries) => { + t.is(entries.length, 14) + t.is(typeof entries[0].count, 'number') +}) + +test(macro, { + interval: 'MONTHLY', + limit: 1 +}, (t, entries) => { + t.is(entries.length, 1) + t.is(typeof entries[0].count, 'number') +}) + +test(macro, { + interval: 'YEARLY' +}, (t, entries) => { + t.is(entries.length, 14) + t.is(typeof entries[0].count, 'number') +}) + +test(macro, { + interval: 'YEARLY', + limit: 1 +}, (t, entries) => { + t.is(entries.length, 1) + t.is(typeof entries[0].count, 'number') +}) \ No newline at end of file diff --git a/test/resolvers/eventStatistics/list.js b/test/resolvers/eventStatistics/list.js new file mode 100644 index 00000000..e6acce89 --- /dev/null +++ b/test/resolvers/eventStatistics/list.js @@ -0,0 +1,71 @@ +'use strict' + +const test = require('ava') +const listen = require('test-listen') + +const server = require('../../../src/server') +const { connectToDatabase, fillDatabase, cleanupDatabase, disconnectFromDatabase } = require('../_utils') +const { getStats } = require('./_utils') + +const base = listen(server) + +test.before(connectToDatabase) +test.beforeEach(fillDatabase) +test.afterEach.always(cleanupDatabase) +test.after.always(disconnectFromDatabase) + +const macro = async (t, variables, assertions) => { + const limit = variables.limit == null ? '' : `, limit: ${ variables.limit }` + + const statistics = await getStats({ + base, + token: t.context.token.id, + eventId: t.context.event.id, + fragment: ` + list(sorting: ${ variables.sorting }, range: ${ variables.range }${ limit }) { + id + count + created + } + ` + }) + + assertions(t, statistics.list) +} + +macro.title = (providedTitle, opts) => `fetch ${ Object.values(opts).join(' and ') } list entries` + +test(macro, { + sorting: 'TOP', + range: 'LAST_6_MONTHS' +}, (t, entries) => { + t.is(entries.length, 14) + t.is(entries[0].id, 'Key 14') + t.is(entries[0].count, 14) +}) + +test(macro, { + sorting: 'RECENT', + range: 'LAST_6_MONTHS' +}, (t, entries) => { + t.is(entries.length, 14) + t.is(entries[0].id, 'Key 1') +}) + +test(macro, { + sorting: 'RECENT', + range: 'LAST_6_MONTHS', + limit: 1 +}, (t, entries) => { + t.is(entries.length, 1) + t.is(entries[0].id, 'Key 1') +}) + +test(macro, { + sorting: 'NEW', + range: 'LAST_6_MONTHS' +}, (t, entries) => { + t.is(entries.length, 14) + t.true(entries[0].id.includes('Key')) + t.is(typeof entries[0].count, 'number') +}) \ No newline at end of file From 533abeb18efd4f2dfc2f67423b35a041ae7c0ea4 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 13:48:18 +0100 Subject: [PATCH 129/208] Syntax adjustments --- test/resolvers/actions.js | 5 ++--- test/resolvers/domains.js | 4 ++-- test/resolvers/events.js | 4 ++-- test/resolvers/records.js | 4 ++-- test/resolvers/tokens.js | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/test/resolvers/actions.js b/test/resolvers/actions.js index c1c916aa..bfac7129 100644 --- a/test/resolvers/actions.js +++ b/test/resolvers/actions.js @@ -9,8 +9,8 @@ const { connectToDatabase, fillDatabase, cleanupDatabase, disconnectFromDatabase const base = listen(server) -let validAction = null -let ignoredAction = null +let validAction +let ignoredAction const defaultKey = uuid() const defaultValue = 1 @@ -24,7 +24,6 @@ test.after.always(disconnectFromDatabase) test.serial('create action', async (t) => { - const body = { query: ` mutation createAction($eventId: ID!, $input: CreateActionInput!) { diff --git a/test/resolvers/domains.js b/test/resolvers/domains.js index a7e1dd63..383db3ed 100644 --- a/test/resolvers/domains.js +++ b/test/resolvers/domains.js @@ -9,7 +9,7 @@ const { connectToDatabase, fillDatabase, cleanupDatabase, disconnectFromDatabase const base = listen(server) -let validDomain = null +let validDomain const defaultTitle = uuid() const updatedTitle = uuid() @@ -25,7 +25,7 @@ test.serial('create domain', async (t) => { query: ` mutation createDomain($input: CreateDomainInput!) { createDomain(input: $input) { - success, + success payload { id title diff --git a/test/resolvers/events.js b/test/resolvers/events.js index 9d0b57d4..4917360e 100644 --- a/test/resolvers/events.js +++ b/test/resolvers/events.js @@ -9,7 +9,7 @@ const { connectToDatabase, fillDatabase, cleanupDatabase, disconnectFromDatabase const base = listen(server) -let validEvent = null +let validEvent const defaultTitle = uuid() const defaultType = 'CHART' @@ -27,7 +27,7 @@ test.serial('create event', async (t) => { query: ` mutation createEvent($input: CreateEventInput!) { createEvent(input: $input) { - success, + success payload { id title diff --git a/test/resolvers/records.js b/test/resolvers/records.js index 29ece379..dea69313 100644 --- a/test/resolvers/records.js +++ b/test/resolvers/records.js @@ -8,8 +8,8 @@ const { connectToDatabase, fillDatabase, cleanupDatabase, disconnectFromDatabase const base = listen(server) -let validRecord = null -let ignoredRecord = null +let validRecord +let ignoredRecord test.before(connectToDatabase) test.beforeEach(fillDatabase) diff --git a/test/resolvers/tokens.js b/test/resolvers/tokens.js index 4b03a2f4..e36fc9dc 100644 --- a/test/resolvers/tokens.js +++ b/test/resolvers/tokens.js @@ -9,7 +9,7 @@ const server = require('../../src/server') const base = listen(server) -let validToken = null +let validToken test.before(connectToDatabase) test.beforeEach(fillDatabase) From 023199fb2545b27a8b5c06e88d40fbb1906af6e7 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 13:48:34 +0100 Subject: [PATCH 130/208] Tests for permanent tokens --- test/resolvers/_utils.js | 4 +- test/resolvers/permanentTokens.js | 151 ++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 test/resolvers/permanentTokens.js diff --git a/test/resolvers/_utils.js b/test/resolvers/_utils.js index b0334f18..57ebf9ae 100644 --- a/test/resolvers/_utils.js +++ b/test/resolvers/_utils.js @@ -5,6 +5,7 @@ const mongoose = require('mongoose') const fetch = require('node-fetch') const Token = require('../../src/models/Token') +const PermanentToken = require('../../src/models/PermanentToken') const Domain = require('../../src/models/Domain') const Event = require('../../src/models/Event') const Record = require('../../src/models/Record') @@ -21,8 +22,9 @@ const connectToDatabase = async () => { } const fillDatabase = async (t) => { - // Saves to context so tests can access IDs + // Saves to context so tests can access ids t.context.token = await Token.create({}) + t.context.permanentToken = await PermanentToken.create({ title: 'Example' }) t.context.domain = await Domain.create({ title: 'Example' }) t.context.event = await Event.create({ title: 'Example', type: 'CHART' }) diff --git a/test/resolvers/permanentTokens.js b/test/resolvers/permanentTokens.js new file mode 100644 index 00000000..4400c750 --- /dev/null +++ b/test/resolvers/permanentTokens.js @@ -0,0 +1,151 @@ +'use strict' + +const test = require('ava') +const listen = require('test-listen') +const uuid = require('uuid').v4 + +const server = require('../../src/server') +const { connectToDatabase, fillDatabase, cleanupDatabase, disconnectFromDatabase, api } = require('./_utils') + +const base = listen(server) + +let validPermanentToken + +const defaultTitle = uuid() +const updatedTitle = uuid() + +test.before(connectToDatabase) +test.beforeEach(fillDatabase) +test.afterEach.always(cleanupDatabase) +test.after.always(disconnectFromDatabase) + +test.serial('create permanent token', async (t) => { + + const body = { + query: ` + mutation createPermanentToken($input: CreatePermanentTokenInput!) { + createPermanentToken(input: $input) { + success + payload { + id + title + } + } + } + `, + variables: { + input: { + title: defaultTitle + } + } + } + + const { json } = await api(base, body, t.context.token.id) + + t.true(json.data.createPermanentToken.success) + t.is(typeof json.data.createPermanentToken.payload.id, 'string') + t.is(json.data.createPermanentToken.payload.title, defaultTitle) + + // Save permanent token for the next test + validPermanentToken = json.data.createPermanentToken.payload + +}) + +test.serial('update permanent token', async (t) => { + + const body = { + query: ` + mutation updatePermanentToken($id: ID!, $input: UpdatePermanentTokenInput!) { + updatePermanentToken(id: $id, input: $input) { + success + payload { + id + title + } + } + } + `, + variables: { + id: validPermanentToken.id, + input: { + title: updatedTitle + } + } + } + + const { json } = await api(base, body, t.context.token.id) + + t.true(json.data.updatePermanentToken.success) + t.is(json.data.updatePermanentToken.payload.id, validPermanentToken.id) + t.is(json.data.updatePermanentToken.payload.title, updatedTitle) + + // Save permanent token for the next test + validPermanentToken = json.data.updatePermanentToken.payload + +}) + +test.serial('fetch permanent tokens', async (t) => { + + const body = { + query: ` + query fetchPermanentTokens { + permanentTokens { + id + title + } + } + ` + } + + const { json } = await api(base, body, t.context.token.id) + + const permanentTokens = json.data.permanentTokens + const permanentToken = permanentTokens.find((permanentToken) => permanentToken.id === validPermanentToken.id) + + t.is(permanentToken.title, validPermanentToken.title) + +}) + +test.serial('fetch permanent token', async (t) => { + + const body = { + query: ` + query fetchPermanentToken($id: ID!) { + permanentToken(id: $id) { + id + title + } + } + `, + variables: { + id: validPermanentToken.id + } + } + + const { json } = await api(base, body, t.context.token.id) + + t.is(json.data.permanentToken.id, validPermanentToken.id) + t.is(json.data.permanentToken.title, validPermanentToken.title) + +}) + +test.serial('delete permanent token', async (t) => { + + const body = { + query: ` + mutation deletePermanentToken($id: ID!) { + deletePermanentToken(id: $id) { + success + } + } + `, + variables: { + id: validPermanentToken.id + } + } + + const { json } = await api(base, body, t.context.token.id) + + t.true(json.data.deletePermanentToken.success) + +}) \ No newline at end of file From 413360ac29e7ee5875f0f843a1a0c21c73e91a4a Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 13:49:51 +0100 Subject: [PATCH 131/208] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a14d48b..0fd2b10b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ### Added - "Copied to clipboard" message when clicking on an input or textarea that copies to the clipboard (#166) +- Tests for permanent tokens, events and actions ### Fixed From 7ca8bac455a51a7df64e13d495d99f4715fd7a76 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 14:40:52 +0100 Subject: [PATCH 132/208] Update changelog --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fd2b10b..5fb4fbeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased (Events)] +Introducing events 🎉 Ackee can now track events like newsletter subscriptions, buttons clicks, checkout sums and more. It's the most requested feature and I'm happy that it's finally a part of Ackee. + +### Breaking changes + +#### New `Access-Control-Allow-Credentials` header + +> This change is relevant for everyone. + +Ackee requires [a new `Access-Control-Allow-Credentials` header](https://github.com/electerious/Ackee/blob/master/docs/CORS%20headers.md#credentials) which was previously optional. Make sure to add this header in your server or reverse proxy configuration. + +#### ackee-tracker with new `.create` and `.record` syntax + +> This change is only relevant for you when using ackee-tracker in the [Manually](https://github.com/electerious/ackee-tracker/blob/master/README.md#manually) or [Programmatic](https://github.com/electerious/ackee-tracker/blob/master/README.md#programmatic) way. + +The [changelog of ackee-tracker](https://github.com/electerious/ackee-tracker/blob/master/CHANGELOG.md) contains everything you need to know when updating to the newest version. + ### Added - "Copied to clipboard" message when clicking on an input or textarea that copies to the clipboard (#166) From ab760c0fd64dfe0f0e42848613c33ad9c5c8da6f Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 14:41:09 +0100 Subject: [PATCH 133/208] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fb4fbeb..9fc9718e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased (Events)] +## [Unreleased (v3)] Introducing events 🎉 Ackee can now track events like newsletter subscriptions, buttons clicks, checkout sums and more. It's the most requested feature and I'm happy that it's finally a part of Ackee. From 773d8621c422069805c211be1b3500f9ee9a1a03 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 14:54:35 +0100 Subject: [PATCH 134/208] Update version and ackee-tracker --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7440de85..c11cc041 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ackee", "private": true, - "version": "2.4.1", + "version": "3.0.0-0", "authors": [ "Tobias Reich " ], @@ -34,7 +34,7 @@ "lint": "eslint '{functions,src,test}/**/*.js'" }, "dependencies": { - "ackee-tracker": "^4.2.0", + "ackee-tracker": "^5.0.0-0", "apollo-server-lambda": "^2.19.1", "apollo-server-micro": "^2.19.1", "apollo-server-plugin-http-headers": "^0.1.4", diff --git a/yarn.lock b/yarn.lock index 5cfc0126..8d5370b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1834,10 +1834,10 @@ accept@^3.0.2: boom "7.x.x" hoek "6.x.x" -ackee-tracker@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/ackee-tracker/-/ackee-tracker-4.2.0.tgz#6bae008eabdaac358c3da79b568e5e2694bed765" - integrity sha512-SEEr7c+6l1+leLmjf61Abkjux+NnFQON3dpoXgbnmYj/MdYcOF2qiaT8Y8jpMspNJ94CN1TKTD9uLcGrmPvBHA== +ackee-tracker@^5.0.0-0: + version "5.0.0-0" + resolved "https://registry.yarnpkg.com/ackee-tracker/-/ackee-tracker-5.0.0-0.tgz#9b334c8b90e50a91fa5697a335c648c3ecf337d1" + integrity sha512-FhfOI/mtoFSF15gLRL9Jw9C63Y3hmZxPN6A22h+0Um8UKnqnNL0JmNhaaw+Rbmbr8MVY8sgfc9pK16aRtQ033g== dependencies: platform "^1.3.6" From a13c42967662f86584c6954a91171bfe8216c8f6 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 15:55:54 +0100 Subject: [PATCH 135/208] Add prebuild and commit a few build files for a faster start #205 --- .gitignore | 5 +- build.js | 89 +--------------------------- dist/.gitkeep | 0 {src/ui/images => dist}/favicon.ico | Bin dist/index.css | 1 + dist/index.html | 25 ++++++++ dist/tracker.js | 1 + package.json | 4 +- prebuild.js | 8 +++ src/ui/index.js | 64 +++++++++++++++++++- src/utils/layout.js | 29 --------- 11 files changed, 106 insertions(+), 120 deletions(-) mode change 100644 => 100755 build.js delete mode 100644 dist/.gitkeep rename {src/ui/images => dist}/favicon.ico (100%) create mode 100644 dist/index.css create mode 100644 dist/index.html create mode 100644 dist/tracker.js create mode 100755 prebuild.js delete mode 100644 src/utils/layout.js diff --git a/.gitignore b/.gitignore index 9639f3e9..d29fdd2c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,10 @@ node_modules # Dist folder dist/* -!dist/.gitkeep +!dist/index.html +!dist/favicon.ico +!dist/index.css +!dist/tracker.js # Vercel .vercel diff --git a/build.js b/build.js old mode 100644 new mode 100755 index 6c55e2f5..5c0feb06 --- a/build.js +++ b/build.js @@ -2,92 +2,9 @@ 'use strict' require('dotenv').config() -const { resolve } = require('path') -const { writeFile, readFile } = require('fs').promises -const sass = require('rosid-handler-sass') -const js = require('rosid-handler-js-next') - -const html = require('./src/ui/index') -const isDemoMode = require('./src/utils/isDemoMode') -const isDevelopmentMode = require('./src/utils/isDevelopmentMode') const customTracker = require('./src/utils/customTracker') -const signale = require('./src/utils/signale') - -const index = async () => { - - const data = html() - - return data - -} - -const favicon = async () => { - - const filePath = resolve(__dirname, 'src/ui/images/favicon.ico') - const data = readFile(filePath) - - return data - -} - -const styles = async () => { - - const filePath = resolve(__dirname, 'src/ui/styles/index.scss') - const data = sass(filePath, { optimize: isDevelopmentMode === false }) - - return data - -} +const { scripts, tracker, build } = require('./src/ui/index') -const scripts = async () => { - - const filePath = resolve(__dirname, 'src/ui/scripts/index.js') - - const data = js(filePath, { - optimize: isDevelopmentMode === false, - replace: { - 'process.env.ACKEE_TRACKER': JSON.stringify(process.env.ACKEE_TRACKER), - 'process.env.ACKEE_DEMO': JSON.stringify(isDemoMode === true ? 'true' : 'false') - }, - nodeGlobals: isDevelopmentMode === true, - babel: false - }) - - return data - -} - -const tracker = async () => { - - const filePath = require.resolve('ackee-tracker') - const data = readFile(filePath, 'utf8') - - return data - -} - -const build = async (path, fn) => { - - try { - signale.await(`Building and writing '${ path }'`) - const data = await fn() - await writeFile(path, data) - signale.success(`Finished building '${ path }'`) - } catch (err) { - signale.fatal(err) - process.exit(1) - } - -} - -// Required files -build('dist/index.html', index) -build('dist/favicon.ico', favicon) -build('dist/index.css', styles) +// Build files that depend on environment variables of the installation build('dist/index.js', scripts) -build('dist/tracker.js', tracker) - -// Optional files -if (customTracker.exists === true) { - build(`dist/${ customTracker.path }`, tracker) -} \ No newline at end of file +if (customTracker.exists === true) build(`dist/${ customTracker.path }`, tracker) \ No newline at end of file diff --git a/dist/.gitkeep b/dist/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/ui/images/favicon.ico b/dist/favicon.ico similarity index 100% rename from src/ui/images/favicon.ico rename to dist/favicon.ico diff --git a/dist/index.css b/dist/index.css new file mode 100644 index 00000000..df0755d2 --- /dev/null +++ b/dist/index.css @@ -0,0 +1 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}.input{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;width:100%;margin:0 0 .9rem;padding:.6rem;background:hsla(0,0%,100%,.05);border:1px solid transparent;border-radius:8px;outline:none;resize:vertical;transition:border-color .3s ease;font-family:inherit;font-size:100%;line-height:1.15}.input,.input[disabled]{box-shadow:none;color:#fff}.input[disabled]{border-color:rgba(13,13,13,0);background-color:hsla(0,0%,94.9%,.05);cursor:not-allowed}.input:focus{border-color:#73fac8}.input::-moz-placeholder{color:#999;opacity:1}.input:-ms-input-placeholder{opacity:1}.input::placeholder{color:#999;opacity:1}.input:-ms-input-placeholder{color:#999}.input::-ms-input-placeholder{color:#999}.control{position:relative;margin:0 0 .9rem;box-sizing:border-box}.control__input{position:absolute;opacity:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin:0;left:0;top:calc(50% - 10px);width:20px;height:20px;pointer-events:none}.control__label{display:flex;align-items:center;position:relative;color:#fff}.control__label:after,.control__label:before{content:"";display:block;width:20px;height:20px;border:1px solid transparent}.control__label:before{flex-shrink:0;transition:border-color .3s ease;margin-right:.6rem;border-color:transparent;box-shadow:none;background:hsla(0,0%,100%,.05)}.control__label:after{position:absolute;top:calc(50% - 11px);left:0;background-size:60%;background-repeat:no-repeat;background-position:50%;transform:scale(0);transition:transform .3s ease}.control__input[type=radio]+.control__label:before{border-radius:100%}.control__input[type=radio]+.control__label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cpath fill='%23fff' d='M256 464c114.9 0 208-93.1 208-208S370.9 48 256 48 48 141.1 48 256s93.1 208 208 208z'/%3E%3C/svg%3E")}.control__input[type=checkbox]+.control__label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cpath fill='%23fff' d='M461.6 109.6l-54.9-43.3c-1.7-1.4-3.8-2.4-6.2-2.4-2.4 0-4.6 1-6.3 2.5L194.5 323s-78.5-75.5-80.7-77.7c-2.2-2.2-5.1-5.9-9.5-5.9s-6.4 3.1-8.7 5.4c-1.7 1.8-29.7 31.2-43.5 45.8-.8.9-1.3 1.4-2 2.1-1.2 1.7-2 3.6-2 5.7 0 2.2.8 4 2 5.7l2.8 2.6s139.3 133.8 141.6 136.1c2.3 2.3 5.1 5.2 9.2 5.2 4 0 7.3-4.3 9.2-6.2l249.1-320c1.2-1.7 2-3.6 2-5.8 0-2.5-1-4.6-2.4-6.4z'/%3E%3C/svg%3E")}.control__input[type=checkbox][disabled]+.control__label,.control__input[type=radio][disabled]+.control__label{cursor:not-allowed;color:#fff}.control__input[type=checkbox][disabled]+.control__label:before,.control__input[type=radio][disabled]+.control__label:before{border-color:rgba(13,13,13,0);background-color:hsla(0,0%,94.9%,.05);box-shadow:none}.control__input:focus+.control__label:before{border-color:#73fac8}.control__input:checked+.control__label:after{transform:scale(1)}.select{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0 0 .9rem;padding:.6rem calc(.9rem + 12px) .6rem .6rem;width:100%;background:hsla(0,0%,100%,.05);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cpath fill='%23fff' d='M256 298.3l174.2-167.2c4.3-4.2 11.4-4.1 15.8.2l30.6 29.9c4.4 4.3 4.5 11.3.2 15.5L264.1 380.9c-2.2 2.2-5.2 3.2-8.1 3-3 .1-5.9-.9-8.1-3L35.2 176.7c-4.3-4.2-4.2-11.2.2-15.5L66 131.3c4.4-4.3 11.5-4.4 15.8-.2L256 298.3z'/%3E%3C/svg%3E");background-size:12px;background-repeat:no-repeat;background-position:calc(100% - .6rem) 50%;border:1px solid transparent;border-radius:8px;outline:0;transition:border-color .3s ease;font-family:inherit;font-size:100%;line-height:1.15}.select,.select[disabled]{box-shadow:none;color:#fff}.select[disabled]{border-color:rgba(13,13,13,0);background-color:hsla(0,0%,94.9%,.05);cursor:not-allowed}.select:focus{border-color:#73fac8}.select::-ms-expand{display:none}html{width:100%;height:100%;font:normal 400 112.5%/1.5 Rubik,sans-serif}body{display:grid;color:hsla(0,0%,100%,.5);background:#282d2d;grid-template-columns:100%;min-height:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body *,body :after,body :before{box-sizing:border-box}.customScrollbar ::-webkit-scrollbar{width:6px;height:6px;z-index:1000}.customScrollbar ::-webkit-scrollbar-track{background:transparent}.customScrollbar ::-webkit-scrollbar-thumb{background-color:hsla(0,0%,100%,.5);border-radius:3px}.customScrollbar ::-webkit-scrollbar-corner{display:none}.customScrollbar *{scrollbar-width:thin;scrollbar-color:hsla(0,0%,100%,.5) transparent}@font-face{font-family:Rubik;src:url("data:application/font-woff2;charset=utf-8;base64,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") format("woff2");font-style:normal;font-weight:400}.headline{margin:0 0 .5rem;color:#fff;font-weight:400;font-size:2.2em;text-rendering:optimizeLegibility;line-height:1}.headline--medium{margin:0 0 .25rem;font-size:1.5em;line-height:1.2}.headline--small{margin:0 0 .3333333333rem;font-size:inherit;line-height:inherit}.headline--no-spacing{margin-bottom:0}.headline__button{text-align:left;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.headline__button:focus{color:#73fac8;outline:none}.text{margin:0 0 1rem}.text--no-spacing{margin-bottom:0}.label{display:block;font-size:.9em;margin:0 0 .25rem}.link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;padding:0;border:none;color:currentColor;font:inherit;border-radius:0;cursor:pointer;text-decoration:none}.input,.select{border-width:2px}.select{padding-right:2rem}.select option{color:#000}.select:disabled{opacity:.6;background-image:none}.control__label{padding-left:calc(24px + .6rem)}.control__label:after,.control__label:before{top:calc(50% - 12px);width:24px;height:24px}.control__label:before{border-width:2px}.inputMessage{display:grid;grid-template-areas:"main";position:relative;margin-bottom:.9rem}@-webkit-keyframes inputMessage__show{0%{opacity:1}60%{opacity:1}to{opacity:0}}@keyframes inputMessage__show{0%{opacity:1}60%{opacity:1}to{opacity:0}}.inputMessage .input{grid-area:main;margin-bottom:0}.inputMessage:after{content:attr(title);grid-area:main;display:flex;align-items:center;justify-content:center;background:rgba(69,74,74,.98);border:2px solid #73fac8;border-radius:8px;color:#fff;font-size:.86em;opacity:0;pointer-events:none}@supports ((-webkit-backdrop-filter:blur(10px)) or (backdrop-filter:blur(10px))){.inputMessage:after{background:rgba(69,74,74,.86);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}}.inputMessage:focus-within:after{-webkit-animation-name:inputMessage__show;animation-name:inputMessage__show;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-duration:2s;animation-duration:2s}#main{display:grid;grid-template-columns:100%}.card{background:#333838;border-radius:10px;width:100%;overflow:hidden}.card--overlay{justify-self:center;align-self:center;max-width:420px}.card--wide{grid-column:1/-1}.card__inner{padding:1.6rem 1.5rem 1.5rem;width:100%}.card__footer{display:flex;border-top:1px solid #282d2d}.card__button{display:flex;justify-content:flex-end;padding:1rem 1.5rem;transition:background .3s ease;outline:none}.card__button[disabled]{cursor:not-allowed}.card__button:not([disabled]):focus,.card__button:not([disabled]):hover{background:hsla(0,0%,100%,.05)}.card__button:active{background:none;transition:none}.card__button--primary{width:100%}.card__separator{flex-shrink:0;width:1px;background:#282d2d}.spinner{position:relative;height:26px;width:26px}@-webkit-keyframes spinner__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}@keyframes spinner__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}.spinner__circle{position:absolute;width:100%;height:100%;border:3px solid transparent;border-radius:100%;-webkit-animation-name:spinner__rotate;animation-name:spinner__rotate;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.spinner__circle--primary{border-right-color:#73fac8;-webkit-animation-duration:.5s;animation-duration:.5s}.spinner__circle--white{border-right-color:#fff;-webkit-animation-duration:.8s;animation-duration:.8s}.spinner__circle--dimmed{border-color:hsla(0,0%,100%,.05)}.updater{position:relative;width:1em;height:1em;border-radius:100%;background:rgba(69,74,74,.86)}@-webkit-keyframes updater__zoom{0%{transform:scale(.2);opacity:0}20%{opacity:.6}80%{transform:scale(.9);opacity:0}to{transform:scale(.9);opacity:0}}@keyframes updater__zoom{0%{transform:scale(.2);opacity:0}20%{opacity:.6}80%{transform:scale(.9);opacity:0}to{transform:scale(.9);opacity:0}}.updater__circle{position:absolute;width:100%;height:100%;background:#fff;border-radius:inherit;-webkit-animation-name:updater__zoom;animation-name:updater__zoom;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-duration:2s;animation-duration:2s}.loader{position:relative;width:1em;height:1em;border-radius:100%;background:rgba(69,74,74,.86)}@-webkit-keyframes loader__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}@keyframes loader__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}.loader__circle{position:absolute;width:100%;height:100%;border-radius:inherit;border:2px solid transparent;border-right-color:#fff;-webkit-animation-name:loader__rotate;animation-name:loader__rotate;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-duration:.5s;animation-duration:.5s;opacity:.6}.message{margin-bottom:1rem;padding:.6rem;border:2px solid transparent;border-radius:8px;color:#fff;font-size:.8em}.message--success{background:rgba(0,255,0,.1)}.message--warning{background:rgba(255,255,0,.1)}.message--error{background:rgba(255,0,0,.15)}.header{position:relative;display:flex;flex-direction:column;align-items:center;border-bottom:1px solid rgba(0,0,0,.3)}.header:after,.header:before{content:"";position:absolute;top:0;bottom:0;width:2rem;pointer-events:none}.header:before{left:0;background:linear-gradient(90deg,#282d2d,rgba(40,45,45,0))}.header:after{right:0;background:linear-gradient(90deg,rgba(40,45,45,0),#282d2d)}@-webkit-keyframes header__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}@keyframes header__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}.header__logo{position:relative;margin:1rem 0 .5rem;width:42px;height:42px;background:linear-gradient(135deg,#73fac8,#00bee1);border-radius:100%}.header__spinner{position:absolute;top:-4px;left:-4px;width:calc(100% + 8px);height:calc(100% + 8px);border-radius:100%;border:2px solid transparent;opacity:0;transition:opacity 1.2s ease;-webkit-animation-name:header__rotate;animation-name:header__rotate;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.header__spinner--visible{opacity:1}.header__spinner--white{border-right-color:#fff;-webkit-animation-duration:.5s;animation-duration:.5s}.header__spinner--primary{border-right-color:#73fac8;-webkit-animation-duration:1.2s;animation-duration:1.2s}.header__nav{display:flex;width:100%;overflow:auto;-webkit-overflow-scrolling:touch}.header__buttons{display:flex;margin:0 auto;padding:0 .5rem}.header__button{display:flex;align-items:center;margin:0 .5rem;padding:.6666666667rem 2px;outline:none;white-space:nowrap;transition:box-shadow .3s ease}.header__button.hovered,.header__button:focus,.header__button:hover{box-shadow:inset 0 -2px 0 hsla(0,0%,100%,.5)}.header__button.active{box-shadow:inset 0 -2px 0 #73fac8}.header__arrow{margin-left:.5rem;width:12px;height:12px;fill:currentColor}.content{--columns:2;display:grid;grid-template-columns:repeat(var(--columns),minmax(0,1fr));gap:1rem;margin:0 auto;padding:6vh 1rem;width:100%;max-width:1200px}@media (max-width:800px){.content{--columns:1}}.content__spacer{height:3vh;grid-column:1/-1}.barChart{position:relative;display:grid;grid-auto-flow:column;grid-template-columns:-webkit-min-content auto;grid-template-columns:min-content auto;gap:.5rem;padding-top:3.5rem;height:300px;overflow:hidden}.barChart__axis{display:flex;flex-direction:column;justify-content:space-between;align-self:stretch;min-width:40px}.barChart__row{position:relative;font-size:.8em;padding:.5rem 0}.barChart__row--top{transform:translateY(-100%)}.barChart__row--middle{transform:translateY(-50%)}.barChart__row--bottom{transform:translateY(0)}.barChart__row:after{content:"";position:absolute;left:0;bottom:0;width:1200px;height:1px;background:hsla(0,0%,100%,.05)}.barChart__columns{display:flex;flex-direction:row-reverse}.barChart__column{display:flex;align-items:flex-end;padding:0 .25rem;width:100%}.barChart__bar{position:relative;width:100%;height:var(--size);min-height:2px;background:#6e7373;transition:height .3s ease}.barChart__column.active .barChart__bar{background:#73fac8}.barChart__column.active .barChart__bar:after{--arrow-width:15px;--arrow-height:10px;content:attr(data-label);position:absolute;right:0;bottom:calc(100% + 1rem);padding:.2em .5em calc(.2em + var(--arrow-height));background:#fff;-webkit-clip-path:polygon(0 0,100% 0,100% 100%,calc(100% - var(--arrow-width)) calc(100% - var(--arrow-height)),0 calc(100% - var(--arrow-height)));clip-path:polygon(0 0,100% 0,100% 100%,calc(100% - var(--arrow-width)) calc(100% - var(--arrow-height)),0 calc(100% - var(--arrow-height)));color:currentColor;z-index:1}.line{margin:0 0 1rem;height:1px;background:hsla(0,0%,100%,.05);border:0}.linkItem{display:flex;justify-content:space-between;align-items:center;margin:0 0 1rem;width:100%;transition:color .3s ease;outline:none}.linkItem--disabled{cursor:inherit}.linkItem:not(.linkItem--disabled):focus,.linkItem:not(.linkItem--disabled):hover{color:#fff}@media (max-width:800px){.linkItem{flex-direction:column;align-items:flex-start;text-align:left}}.flexList{position:relative}.flexList:after,.flexList:before{content:"";position:absolute;left:0;right:0;height:.5rem;pointer-events:none;z-index:1}.flexList:before{top:0;background:linear-gradient(#333838,rgba(51,56,56,0))}.flexList:after{bottom:0;background:linear-gradient(rgba(51,56,56,0),#333838)}.flexList__inner{padding-top:1rem;height:300px;overflow:auto;-webkit-overflow-scrolling:touch}.flexList__row{position:relative;display:flex;align-items:center;height:calc(32px + 1rem);border-bottom:1px solid hsla(0,0%,100%,.05);font-size:.9em}.flexList__row:last-child{border-bottom:none}.flexList__row--has-hover,.flexList__row[href]{color:currentColor;text-decoration:none;transition:color .3s ease}.flexList__row--has-hover:hover,.flexList__row[href]:hover{color:#fff}.flexList__column{display:grid;grid-auto-flow:column}.flexList__column--fixed-width{width:var(--width);flex-shrink:0}.flexList__column--spacing-left{margin-left:1rem}.flexList__column--spacing-right{margin-right:1rem}.flexList__column--text-adjustment{margin-top:3px}.flexList__bar{position:absolute;left:0;top:.5rem;width:var(--width);height:32px;border-radius:100px;background:hsla(0,0%,100%,.05);pointer-events:none}.flexList__bar--favicon{min-width:32px}.flexList__bar--counter{min-width:52px}.flexList__obscured,.flexList__truncated{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.flexList__obscured{opacity:0;transition:opacity .3s ease}.flexList__row:hover .flexList__obscured{opacity:.6}.emptyState{display:grid;align-content:center;justify-content:center;height:300px}.emptyState__inner{display:grid;align-items:center;grid-auto-flow:column;gap:.6666666667rem}.emptyState__icon{fill:hsla(0,0%,100%,.5);width:24px;height:24px}.valueText{display:grid;grid-auto-flow:column;justify-content:start;align-items:end;gap:.5rem}.favicon{width:32px;height:32px;background:#fff;border-radius:100%;border:2px solid #fff;overflow:hidden}.favicon--missing{background:linear-gradient(135deg,#73fac8,#00bee1)}.modal{position:fixed;display:flex;align-items:center;justify-content:center;top:0;right:0;bottom:0;left:0;padding:0 1rem;background:rgba(26,29,29,.9);pointer-events:none;opacity:0;z-index:3;transition:opacity .3s ease}.modal.visible{opacity:1;pointer-events:all}.modal__inner{width:100%;max-width:600px;max-height:100%;overflow:auto;-webkit-overflow-scrolling:touch;-ms-scroll-chaining:none;overscroll-behavior:contain;transform:translateY(20px);transition:transform .3s ease}.modal.visible .modal__inner{transform:none}.context{position:absolute;display:grid;top:0;left:0;padding:.5rem 0;min-width:210px;background:rgba(69,74,74,.98);border-radius:10px;transform:translate(var(--x),var(--y));z-index:3;opacity:0;pointer-events:none}@supports ((-webkit-backdrop-filter:blur(10px)) or (backdrop-filter:blur(10px))){.context{background:rgba(69,74,74,.86);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}}@media (max-width:800px){.context{font-size:.9em}}.context--floating{position:fixed;min-width:210px;border-radius:12px}.context.visible{opacity:1;pointer-events:all}.context__button{padding:.3333333333rem 1.2rem;color:currentColor;text-align:left;transition:color .3s ease,background .2s ease;line-height:1.4;outline:none}.context__button.active{color:#fff}.context__button:focus,.context__button:hover{background:hsla(0,0%,100%,.05)}.context--floating .context__button{margin:0 .5rem;padding:.5rem .625rem;color:#fff;border-radius:8px}.context--floating .context__button.active{color:#73fac8}.context__head{display:grid;grid-auto-flow:column;gap:1rem;align-items:center;justify-content:space-between}.context__label{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.context__description{padding-right:1rem;font-size:.86em;color:hsla(0,0%,100%,.5)}.context__separator{height:1px;background:hsla(0,0%,100%,.05);margin:.5rem 0}.filter{position:fixed;display:grid;justify-content:center;width:100%;bottom:4vh;z-index:2;pointer-events:none}@media (max-width:800px){.filter{font-size:.9em}}.filter__bar{display:grid;grid-auto-flow:column;gap:1rem;padding:0 1.5rem;border-radius:100px;background:rgba(69,74,74,.98);pointer-events:all}@supports ((-webkit-backdrop-filter:blur(10px)) or (backdrop-filter:blur(10px))){.filter__bar{background:rgba(69,74,74,.86);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}}.filter__button{display:flex;align-items:center;padding:.6666666667rem 0;white-space:nowrap;color:#fff;outline:none}.filter__button:focus{color:#73fac8}.filter__arrow{margin-left:.5rem;width:12px;height:12px;fill:hsla(0,0%,100%,.5)}.keyHint{display:flex;justify-content:center;width:1.5em;height:1.5em;font-size:.8em;background:hsla(0,0%,100%,.05);border-radius:3px;border:1px solid #6e7373;color:hsla(0,0%,100%,.5)}.keyHint,.status{align-items:center}.status{display:grid;gap:.5rem;grid-auto-flow:column;justify-content:start}.facts{--columns:3;grid-column:1/-1;display:grid;grid-template-columns:repeat(var(--columns),minmax(0,1fr));gap:1px;border-radius:10px;width:100%;overflow:hidden}@media (max-width:900px){.facts{--columns:2}}@media (max-width:560px){.facts{--columns:1}}.facts__card{display:grid;align-content:space-between;gap:1rem;background:#333838;padding:1.6rem 1.5rem 1.5rem;width:100%}.align-left{text-align:left}.align-right{text-align:right}.align-center{text-align:center}.spacer{height:calc(1rem*var(--size))}.color-primary{color:#73fac8}.color-white{color:#fff}.color-light{color:hsla(0,0%,100%,.5)}.color-black{color:#282d2d}.color-destructive{color:#ff3c3c} \ No newline at end of file diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 00000000..6d0a4b91 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,25 @@ + + + + + Ackee + + + + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/dist/tracker.js b/dist/tracker.js new file mode 100644 index 00000000..3cdb73d5 --- /dev/null +++ b/dist/tracker.js @@ -0,0 +1 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).ackeeTracker=e()}}((function(){return function e(t,n,r){function i(a,l){if(!n[a]){if(!t[a]){var s="function"==typeof require&&require;if(!l&&s)return s(a,!0);if(o)return o(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var c=n[a]={exports:{}};t[a][0].call(c.exports,(function(e){return i(t[a][1][e]||e)}),c,c.exports,e,t,n,r)}return n[a].exports}for(var o="function"==typeof require&&require,a=0;a-1&&r<=s)for(;++n3?"WebKit":/\bOpera\b/.test(N)&&(/\bOPR\b/.test(t)?"Blink":"Presto"))||/\b(?:Midori|Nook|Safari)\b/i.test(t)&&!/^(?:Trident|EdgeHTML)$/.test(G)&&"WebKit"||!G&&/\bMSIE\b/i.test(t)&&("Mac OS"==K?"Tasman":"Trident")||"WebKit"==G&&/\bPlayStation\b(?! Vita\b)/i.test(N)&&"NetFront")&&(G=[l]),"IE"==N&&(l=(/; *(?:XBLWP|ZuneWP)(\d+)/i.exec(t)||0)[1])?(N+=" Mobile",K="Windows Phone "+(/\+$/.test(l)?l:l+".x"),$.unshift("desktop mode")):/\bWPDesktop\b/i.test(t)?(N="IE Mobile",K="Windows Phone 8.x",$.unshift("desktop mode"),j||(j=(/\brv:([\d.]+)/.exec(t)||0)[1])):"IE"!=N&&"Trident"==G&&(l=/\brv:([\d.]+)/.exec(t))&&(N&&$.push("identifying as "+N+(j?" "+j:"")),N="IE",j=l[1]),F){if(d="global",p=null!=(c=n)?typeof c[d]:"number",/^(?:boolean|number|string|undefined)$/.test(p)||"object"==p&&!c[d])g(l=n.runtime)==w?(N="Adobe AIR",K=l.flash.system.Capabilities.os):g(l=n.phantom)==M?(N="PhantomJS",j=(l=l.version||null)&&l.major+"."+l.minor+"."+l.patch):"number"==typeof k.documentMode&&(l=/\bTrident\/(\d+)/i.exec(t))?(j=[j,k.documentMode],(l=+l[1]+4)!=j[1]&&($.push("IE "+j[1]+" mode"),G&&(G[1]=""),j[1]=l),j="IE"==N?String(j[1].toFixed(1)):j[0]):"number"==typeof k.documentMode&&/^(?:Chrome|Firefox)\b/.test(N)&&($.push("masking as "+N+" "+j),N="IE",j="11.0",G=["Trident"],K="Windows");else if(P&&(W=(l=P.lang.System).getProperty("os.arch"),K=K||l.getProperty("os.name")+" "+l.getProperty("os.version")),I){try{j=n.require("ringo/engine").version.join("."),N="RingoJS"}catch(e){(l=n.system)&&l.global.system==n.system&&(N="Narwhal",K||(K=l[0].os||null))}N||(N="Rhino")}else"object"==typeof n.process&&!n.process.browser&&(l=n.process)&&("object"==typeof l.versions&&("string"==typeof l.versions.electron?($.push("Node "+l.versions.node),N="Electron",j=l.versions.electron):"string"==typeof l.versions.nw&&($.push("Chromium "+j,"Node "+l.versions.node),N="NW.js",j=l.versions.nw)),N||(N="Node.js",W=l.arch,K=l.platform,j=(j=/[\d.]+/.exec(l.version))?j[0]:null));K=K&&f(K)}if(j&&(l=/(?:[ab]|dp|pre|[ab]\d+pre)(?:\d+\+?)?$/i.exec(j)||/(?:alpha|beta)(?: ?\d)?/i.exec(t+";"+(F&&o.appMinorVersion))||/\bMinefield\b/i.test(t)&&"a")&&(T=/b/i.test(l)?"beta":"alpha",j=j.replace(RegExp(l+"\\+?$"),"")+("beta"==T?C:A)+(/\d+\+?/.exec(l)||"")),"Fennec"==N||"Firefox"==N&&/\b(?:Android|Firefox OS|KaiOS)\b/.test(K))N="Firefox Mobile";else if("Maxthon"==N&&j)j=j.replace(/\.[\d.]+/,".x");else if(/\bXbox\b/i.test(X))"Xbox 360"==X&&(K=null),"Xbox 360"==X&&/\bIEMobile\b/.test(t)&&$.unshift("mobile mode");else if(!/^(?:Chrome|IE|Opera)$/.test(N)&&(!N||X||/Browser|Mobi/.test(N))||"Windows CE"!=K&&!/Mobi/i.test(t))if("IE"==N&&F)try{null===n.external&&$.unshift("platform preview")}catch(e){$.unshift("embedded")}else(/\bBlackBerry\b/.test(X)||/\bBB10\b/.test(t))&&(l=(RegExp(X.replace(/ +/g," *")+"/([.\\d]+)","i").exec(t)||0)[1]||j)?(K=((l=[l,/BB10/.test(t)])[1]?(X=null,D="BlackBerry"):"Device Software")+" "+l[0],j=null):this!=h&&"Wii"!=X&&(F&&R||/Opera/.test(N)&&/\b(?:MSIE|Firefox)\b/i.test(t)||"Firefox"==N&&/\bOS X (?:\d+\.){2,}/.test(K)||"IE"==N&&(K&&!/^Win/.test(K)&&j>5.5||/\bWindows XP\b/.test(K)&&j>8||8==j&&!/\bTrident\b/.test(t)))&&!u.test(l=e.call(h,t.replace(u,"")+";"))&&l.name&&(l="ing as "+l.name+((l=l.version)?" "+l:""),u.test(N)?(/\bIE\b/.test(l)&&"Mac OS"==K&&(K=null),l="identify"+l):(l="mask"+l,N=B?f(B.replace(/([a-z])([A-Z])/g,"$1 $2")):"Opera",/\bIE\b/.test(l)&&(K=null),F||(j=null)),G=["Presto"],$.push(l));else N+=" Mobile";(l=(/\bAppleWebKit\/([\d.]+\+?)/i.exec(t)||0)[1])&&(l=[parseFloat(l.replace(/\.(\d)$/,".0$1")),l],"Safari"==N&&"+"==l[1].slice(-1)?(N="WebKit Nightly",T="alpha",j=l[1].slice(0,-1)):j!=l[1]&&j!=(l[2]=(/\bSafari\/([\d.]+\+?)/i.exec(t)||0)[1])||(j=null),l[1]=(/\b(?:Headless)?Chrome\/([\d.]+)/i.exec(t)||0)[1],537.36==l[0]&&537.36==l[2]&&parseFloat(l[1])>=28&&"WebKit"==G&&(G=["Blink"]),F&&(y||l[1])?(G&&(G[1]="like Chrome"),l=l[1]||((l=l[0])<530?1:l<532?2:l<532.05?3:l<533?4:l<534.03?5:l<534.07?6:l<534.1?7:l<534.13?8:l<534.16?9:l<534.24?10:l<534.3?11:l<535.01?12:l<535.02?"13+":l<535.07?15:l<535.11?16:l<535.19?17:l<536.05?18:l<536.1?19:l<537.01?20:l<537.11?"21+":l<537.13?23:l<537.18?24:l<537.24?25:l<537.36?26:"Blink"!=G?"27":"28")):(G&&(G[1]="like Safari"),l=(l=l[0])<400?1:l<500?2:l<526?3:l<533?4:l<534?"4+":l<535?5:l<537?6:l<538?7:l<601?8:l<602?9:l<604?10:l<606?11:l<608?12:"12"),G&&(G[1]+=" "+(l+="number"==typeof l?".x":/[.+]/.test(l)?"":"+")),"Safari"==N&&(!j||parseInt(j)>45)?j=l:"Chrome"==N&&/\bHeadlessChrome/i.test(t)&&$.unshift("headless")),"Opera"==N&&(l=/\bzbov|zvav$/.exec(K))?(N+=" ",$.unshift("desktop mode"),"zvav"==l?(N+="Mini",j=null):N+="Mobile",K=K.replace(RegExp(" *"+l+"$"),"")):"Safari"==N&&/\bChrome\b/.exec(G&&G[1])?($.unshift("desktop mode"),N="Chrome Mobile",j=null,/\bOS X\b/.test(K)?(D="Apple",K="iOS 4.3+"):K=null):/\bSRWare Iron\b/.test(N)&&!j&&(j=_("Chrome")),j&&0==j.indexOf(l=/[\d.]+$/.exec(K))&&t.indexOf("/"+l+"-")>-1&&(K=x(K.replace(l,""))),K&&-1!=K.indexOf(N)&&!RegExp(N+" OS").test(K)&&(K=K.replace(RegExp(" *"+S(N)+" *"),"")),G&&!/\b(?:Avant|Nook)\b/.test(N)&&(/Browser|Lunascape|Maxthon/.test(N)||"Safari"!=N&&/^iOS/.test(K)&&/\bSafari\b/.test(G[1])||/^(?:Adobe|Arora|Breach|Midori|Opera|Phantom|Rekonq|Rock|Samsung Internet|Sleipnir|SRWare Iron|Vivaldi|Web)/.test(N)&&G[1])&&(l=G[G.length-1])&&$.push(l),$.length&&($=["("+$.join("; ")+")"]),D&&X&&X.indexOf(D)<0&&$.push("on "+D),X&&$.push((/^on /.test($[$.length-1])?"":"on ")+X),K&&(l=/ ([\d.+]+)$/.exec(K),s=l&&"/"==K.charAt(K.length-l[0].length-1),K={architecture:32,family:l&&!s?K.replace(l[0],""):K,version:l?l[1]:null,toString:function(){var e=this.version;return this.family+(e&&!s?" "+e:"")+(64==this.architecture?" 64-bit":"")}}),(l=/\b(?:AMD|IA|Win|WOW|x86_|x)64\b/i.exec(W))&&!/\bi686\b/i.test(W)?(K&&(K.architecture=64,K.family=K.family.replace(RegExp(" *"+l),"")),N&&(/\bWOW64\b/i.test(t)||F&&/\w(?:86|32)$/.test(o.cpuClass||o.platform)&&!/\bWin64; x64\b/i.test(t))&&$.unshift("32-bit")):K&&/^OS X/.test(K.family)&&"Chrome"==N&&parseFloat(j)>=39&&(K.architecture=64),t||(t=null);var H={};return H.description=t,H.layout=G&&G[0],H.manufacturer=D,H.name=N,H.prerelease=T,H.product=X,H.ua=t,H.version=N&&j,H.os=K||{architecture:null,family:null,version:null,toString:function(){return"null"}},H.parse=e,H.toString=function(){return this.description||""},H.version&&$.unshift(j),H.name&&$.unshift(N),K&&N&&(K!=String(K).split(" ")[0]||K!=N.split(" ")[0]&&!X)&&$.push(X?"("+K+")":"on "+K),$.length&&(H.description=$.join(" ")),H}();o&&a?h(y,(function(e,t){o[t]=e})):i.platform=y}).call(this)}).call(this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],2:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.create=n.detect=n.attributes=void 0;var r,i=(r=e("platform"))&&r.__esModule?r:{default:r};function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function a(e){for(var t=1;t0&&void 0!==arguments[0]&&arguments[0],t={siteLocation:window.location.href,siteReferrer:document.referrer},n={siteLanguage:(navigator.language||navigator.userLanguage).substr(0,2),screenWidth:screen.width,screenHeight:screen.height,screenColorDepth:screen.colorDepth,deviceName:i.default.product,deviceManufacturer:i.default.manufacturer,osName:i.default.os.family,osVersion:i.default.os.version,browserName:i.default.name,browserVersion:i.default.version,browserWidth:window.outerWidth,browserHeight:window.outerHeight};return a(a({},t),!0===e?n:{})};n.attributes=c;var d=function(e,t){return{query:"\n\t\t\tmutation createRecord($domainId: ID!, $input: CreateRecordInput!) {\n\t\t\t\tcreateRecord(domainId: $domainId, input: $input) {\n\t\t\t\t\tpayload {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t",variables:{domainId:e,input:t}}},b=function(e){return{query:"\n\t\t\tmutation updateRecord($recordId: ID!) {\n\t\t\t\tupdateRecord(id: $recordId) {\n\t\t\t\t\tsuccess\n\t\t\t\t}\n\t\t\t}\n\t\t",variables:{recordId:e}}},p=function(e,t,n){var r=new XMLHttpRequest;r.open("POST",e),r.onload=function(){if(200!==r.status)throw new Error("Server returned with an unhandled status");var e=null;try{e=JSON.parse(r.responseText)}catch(e){throw new Error("Failed to parse response from server")}if(null!=e.errors)throw new Error(e.errors[0].message);if("function"==typeof n)return n(e)},r.setRequestHeader("Content-Type","application/json;charset=UTF-8"),r.withCredentials=!0,r.send(JSON.stringify(t))},f=function(){var e=document.querySelector("[data-ackee-domain-id]");if(null!=e){var t=e.getAttribute("data-ackee-server")||"",n=e.getAttribute("data-ackee-domain-id"),r=e.getAttribute("data-ackee-opts")||"{}";h(t,JSON.parse(r)).record(n)}};n.detect=f;var h=function(e,t){t=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t={};return t.detailed=!0===e.detailed,t.ignoreLocalhost=!1!==e.ignoreLocalhost,t}(t);var n,r,i=function(e){var t="/"===e.substr(-1);return e+(!0===t?"":"/")+"api"}(e),o=function(){},a={record:function(){return{stop:o}},updateRecord:function(){return{stop:o}},action:o,updateAction:o};if(!0===t.ignoreLocalhost&&!0==(""===(n=location.hostname)||"localhost"===n||"127.0.0.1"===n||"::1"===n))return console.warn("Ackee ignores you because you are on localhost"),a;if(!0===(r=navigator.userAgent,/bot|crawler|spider|crawling/i.test(r)))return console.warn("Ackee ignores you because you are a bot"),a;return{record:function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:c(t.detailed),r=arguments.length>2?arguments[2]:void 0,o=!1,a=function(){o=!0};return p(i,d(e,n),(function(e){var t=e.data.createRecord.payload.id;if(!0===u(t))return console.warn("Ackee ignores you because this is your own site");var n=setInterval((function(){!0!==o?p(i,b(t)):clearInterval(n)}),15e3);return"function"==typeof r?r(t):void 0})),{stop:a}},updateRecord:function(e){var t=!1,n=function(){t=!0};if(!0===u(e))return console.warn("Ackee ignores you because this is your own site"),{stop:n};var r=setInterval((function(){!0!==t?p(i,b(e)):clearInterval(r)}),15e3);return{stop:n}},action:function(e,t,n){p(i,function(e,t){return{query:"\n\t\t\tmutation createAction($eventId: ID!, $input: CreateActionInput!) {\n\t\t\t\tcreateAction(eventId: $eventId, input: $input) {\n\t\t\t\t\tpayload {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t",variables:{eventId:e,input:t}}}(e,t),(function(e){var t=e.data.createAction.payload.id;return!0===u(t)?console.warn("Ackee ignores you because this is your own site"):"function"==typeof n?n(t):void 0}))},updateAction:function(e,t){if(!0===u(e))return console.warn("Ackee ignores you because this is your own site");p(i,function(e,t){return{query:"\n\t\t\tmutation updateAction($actionId: ID!, $input: UpdateActionInput!) {\n\t\t\t\tupdateAction(id: $actionId, input: $input) {\n\t\t\t\t\tsuccess\n\t\t\t\t}\n\t\t\t}\n\t\t",variables:{actionId:e,input:t}}}(e,t))}}};n.create=h,!0===s&&f()},{platform:1}]},{},[2])(2)})); \ No newline at end of file diff --git a/package.json b/package.json index c11cc041..1012cc50 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "scripts": { "start": "npm run build && npm run server", "start:dev": "NODE_ENV=development nodemon", + "build:pre": "node prebuild.js", "build": "node build.js", "build:dev": "NODE_ENV=development npm run build", "server": "node src/index.js", @@ -105,7 +106,7 @@ "extends": "@electerious/eslint-config" }, "nodemonConfig": { - "exec": "npm start", + "exec": "npm run build:pre && npm start", "ext": "js,json,graphql,scss", "watch": [ "src" @@ -113,6 +114,7 @@ }, "husky": { "hooks": { + "pre-push": "npm run build:pre", "pre-commit": "npm run lint" } }, diff --git a/prebuild.js b/prebuild.js new file mode 100755 index 00000000..d1059ca1 --- /dev/null +++ b/prebuild.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node +'use strict' + +const { styles, tracker, build } = require('./src/ui/index') + +// Build files that are identical on every installation +build('dist/index.css', styles) +build('dist/tracker.js', tracker) \ No newline at end of file diff --git a/src/ui/index.js b/src/ui/index.js index 54978a86..b7225ee5 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -1,9 +1,67 @@ 'use strict' -const layout = require('../utils/layout') +const { resolve } = require('path') +const { writeFile, readFile } = require('fs').promises +const sass = require('rosid-handler-sass') +const js = require('rosid-handler-js-next') -module.exports = () => { +const isDemoMode = require('../utils/isDemoMode') +const isDevelopmentMode = require('../utils/isDevelopmentMode') +const signale = require('../utils/signale') - return layout('
', 'favicon.ico', [ 'index.css' ], [ 'index.js' ]) +const styles = async () => { + const filePath = resolve(__dirname, './styles/index.scss') + const data = sass(filePath, { optimize: isDevelopmentMode === false }) + + return data + +} + +const scripts = async () => { + + const filePath = resolve(__dirname, './scripts/index.js') + + const data = js(filePath, { + optimize: isDevelopmentMode === false, + replace: { + 'process.env.ACKEE_TRACKER': JSON.stringify(process.env.ACKEE_TRACKER), + 'process.env.ACKEE_DEMO': JSON.stringify(isDemoMode === true ? 'true' : 'false') + }, + nodeGlobals: isDevelopmentMode === true, + babel: false + }) + + return data + +} + +const tracker = async () => { + + const filePath = require.resolve('ackee-tracker') + const data = readFile(filePath, 'utf8') + + return data + +} + +const build = async (path, fn) => { + + try { + signale.await(`Building and writing '${ path }'`) + const data = await fn() + await writeFile(path, data) + signale.success(`Finished building '${ path }'`) + } catch (err) { + signale.fatal(err) + process.exit(1) + } + +} + +module.exports = { + styles, + scripts, + tracker, + build } \ No newline at end of file diff --git a/src/utils/layout.js b/src/utils/layout.js deleted file mode 100644 index 43430b82..00000000 --- a/src/utils/layout.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict' - -module.exports = (body, favicon, styles, scripts) => ` - - - - - Ackee - - - - - - - - - ${ styles.map((src) => ``).join('') } - - - ${ scripts.map((src) => ``).join('') } - - - - - ${ body } - - - -` \ No newline at end of file From 8226721172e5d7cb786764db4ed061f0c095045c Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 16:18:47 +0100 Subject: [PATCH 136/208] Prepare build changes --- build.js | 4 ++-- dist/index.css | 1 - dist/index.html | 25 ------------------------- dist/tracker.js | 1 - prebuild.js | 3 ++- src/ui/index.js | 11 +++++++++++ src/utils/layout.js | 34 ++++++++++++++++++++++++++++++++++ 7 files changed, 49 insertions(+), 30 deletions(-) delete mode 100644 dist/index.css delete mode 100644 dist/index.html delete mode 100644 dist/tracker.js create mode 100644 src/utils/layout.js diff --git a/build.js b/build.js index 5c0feb06..1bcf7e7e 100755 --- a/build.js +++ b/build.js @@ -3,8 +3,8 @@ require('dotenv').config() const customTracker = require('./src/utils/customTracker') -const { scripts, tracker, build } = require('./src/ui/index') +const { index, tracker, build } = require('./src/ui/index') // Build files that depend on environment variables of the installation -build('dist/index.js', scripts) +build(`dist/index.html`, index) if (customTracker.exists === true) build(`dist/${ customTracker.path }`, tracker) \ No newline at end of file diff --git a/dist/index.css b/dist/index.css deleted file mode 100644 index df0755d2..00000000 --- a/dist/index.css +++ /dev/null @@ -1 +0,0 @@ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}.input{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;width:100%;margin:0 0 .9rem;padding:.6rem;background:hsla(0,0%,100%,.05);border:1px solid transparent;border-radius:8px;outline:none;resize:vertical;transition:border-color .3s ease;font-family:inherit;font-size:100%;line-height:1.15}.input,.input[disabled]{box-shadow:none;color:#fff}.input[disabled]{border-color:rgba(13,13,13,0);background-color:hsla(0,0%,94.9%,.05);cursor:not-allowed}.input:focus{border-color:#73fac8}.input::-moz-placeholder{color:#999;opacity:1}.input:-ms-input-placeholder{opacity:1}.input::placeholder{color:#999;opacity:1}.input:-ms-input-placeholder{color:#999}.input::-ms-input-placeholder{color:#999}.control{position:relative;margin:0 0 .9rem;box-sizing:border-box}.control__input{position:absolute;opacity:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin:0;left:0;top:calc(50% - 10px);width:20px;height:20px;pointer-events:none}.control__label{display:flex;align-items:center;position:relative;color:#fff}.control__label:after,.control__label:before{content:"";display:block;width:20px;height:20px;border:1px solid transparent}.control__label:before{flex-shrink:0;transition:border-color .3s ease;margin-right:.6rem;border-color:transparent;box-shadow:none;background:hsla(0,0%,100%,.05)}.control__label:after{position:absolute;top:calc(50% - 11px);left:0;background-size:60%;background-repeat:no-repeat;background-position:50%;transform:scale(0);transition:transform .3s ease}.control__input[type=radio]+.control__label:before{border-radius:100%}.control__input[type=radio]+.control__label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cpath fill='%23fff' d='M256 464c114.9 0 208-93.1 208-208S370.9 48 256 48 48 141.1 48 256s93.1 208 208 208z'/%3E%3C/svg%3E")}.control__input[type=checkbox]+.control__label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cpath fill='%23fff' d='M461.6 109.6l-54.9-43.3c-1.7-1.4-3.8-2.4-6.2-2.4-2.4 0-4.6 1-6.3 2.5L194.5 323s-78.5-75.5-80.7-77.7c-2.2-2.2-5.1-5.9-9.5-5.9s-6.4 3.1-8.7 5.4c-1.7 1.8-29.7 31.2-43.5 45.8-.8.9-1.3 1.4-2 2.1-1.2 1.7-2 3.6-2 5.7 0 2.2.8 4 2 5.7l2.8 2.6s139.3 133.8 141.6 136.1c2.3 2.3 5.1 5.2 9.2 5.2 4 0 7.3-4.3 9.2-6.2l249.1-320c1.2-1.7 2-3.6 2-5.8 0-2.5-1-4.6-2.4-6.4z'/%3E%3C/svg%3E")}.control__input[type=checkbox][disabled]+.control__label,.control__input[type=radio][disabled]+.control__label{cursor:not-allowed;color:#fff}.control__input[type=checkbox][disabled]+.control__label:before,.control__input[type=radio][disabled]+.control__label:before{border-color:rgba(13,13,13,0);background-color:hsla(0,0%,94.9%,.05);box-shadow:none}.control__input:focus+.control__label:before{border-color:#73fac8}.control__input:checked+.control__label:after{transform:scale(1)}.select{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0 0 .9rem;padding:.6rem calc(.9rem + 12px) .6rem .6rem;width:100%;background:hsla(0,0%,100%,.05);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cpath fill='%23fff' d='M256 298.3l174.2-167.2c4.3-4.2 11.4-4.1 15.8.2l30.6 29.9c4.4 4.3 4.5 11.3.2 15.5L264.1 380.9c-2.2 2.2-5.2 3.2-8.1 3-3 .1-5.9-.9-8.1-3L35.2 176.7c-4.3-4.2-4.2-11.2.2-15.5L66 131.3c4.4-4.3 11.5-4.4 15.8-.2L256 298.3z'/%3E%3C/svg%3E");background-size:12px;background-repeat:no-repeat;background-position:calc(100% - .6rem) 50%;border:1px solid transparent;border-radius:8px;outline:0;transition:border-color .3s ease;font-family:inherit;font-size:100%;line-height:1.15}.select,.select[disabled]{box-shadow:none;color:#fff}.select[disabled]{border-color:rgba(13,13,13,0);background-color:hsla(0,0%,94.9%,.05);cursor:not-allowed}.select:focus{border-color:#73fac8}.select::-ms-expand{display:none}html{width:100%;height:100%;font:normal 400 112.5%/1.5 Rubik,sans-serif}body{display:grid;color:hsla(0,0%,100%,.5);background:#282d2d;grid-template-columns:100%;min-height:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body *,body :after,body :before{box-sizing:border-box}.customScrollbar ::-webkit-scrollbar{width:6px;height:6px;z-index:1000}.customScrollbar ::-webkit-scrollbar-track{background:transparent}.customScrollbar ::-webkit-scrollbar-thumb{background-color:hsla(0,0%,100%,.5);border-radius:3px}.customScrollbar ::-webkit-scrollbar-corner{display:none}.customScrollbar *{scrollbar-width:thin;scrollbar-color:hsla(0,0%,100%,.5) transparent}@font-face{font-family:Rubik;src:url("data:application/font-woff2;charset=utf-8;base64,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") format("woff2");font-style:normal;font-weight:400}.headline{margin:0 0 .5rem;color:#fff;font-weight:400;font-size:2.2em;text-rendering:optimizeLegibility;line-height:1}.headline--medium{margin:0 0 .25rem;font-size:1.5em;line-height:1.2}.headline--small{margin:0 0 .3333333333rem;font-size:inherit;line-height:inherit}.headline--no-spacing{margin-bottom:0}.headline__button{text-align:left;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.headline__button:focus{color:#73fac8;outline:none}.text{margin:0 0 1rem}.text--no-spacing{margin-bottom:0}.label{display:block;font-size:.9em;margin:0 0 .25rem}.link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;padding:0;border:none;color:currentColor;font:inherit;border-radius:0;cursor:pointer;text-decoration:none}.input,.select{border-width:2px}.select{padding-right:2rem}.select option{color:#000}.select:disabled{opacity:.6;background-image:none}.control__label{padding-left:calc(24px + .6rem)}.control__label:after,.control__label:before{top:calc(50% - 12px);width:24px;height:24px}.control__label:before{border-width:2px}.inputMessage{display:grid;grid-template-areas:"main";position:relative;margin-bottom:.9rem}@-webkit-keyframes inputMessage__show{0%{opacity:1}60%{opacity:1}to{opacity:0}}@keyframes inputMessage__show{0%{opacity:1}60%{opacity:1}to{opacity:0}}.inputMessage .input{grid-area:main;margin-bottom:0}.inputMessage:after{content:attr(title);grid-area:main;display:flex;align-items:center;justify-content:center;background:rgba(69,74,74,.98);border:2px solid #73fac8;border-radius:8px;color:#fff;font-size:.86em;opacity:0;pointer-events:none}@supports ((-webkit-backdrop-filter:blur(10px)) or (backdrop-filter:blur(10px))){.inputMessage:after{background:rgba(69,74,74,.86);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}}.inputMessage:focus-within:after{-webkit-animation-name:inputMessage__show;animation-name:inputMessage__show;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-duration:2s;animation-duration:2s}#main{display:grid;grid-template-columns:100%}.card{background:#333838;border-radius:10px;width:100%;overflow:hidden}.card--overlay{justify-self:center;align-self:center;max-width:420px}.card--wide{grid-column:1/-1}.card__inner{padding:1.6rem 1.5rem 1.5rem;width:100%}.card__footer{display:flex;border-top:1px solid #282d2d}.card__button{display:flex;justify-content:flex-end;padding:1rem 1.5rem;transition:background .3s ease;outline:none}.card__button[disabled]{cursor:not-allowed}.card__button:not([disabled]):focus,.card__button:not([disabled]):hover{background:hsla(0,0%,100%,.05)}.card__button:active{background:none;transition:none}.card__button--primary{width:100%}.card__separator{flex-shrink:0;width:1px;background:#282d2d}.spinner{position:relative;height:26px;width:26px}@-webkit-keyframes spinner__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}@keyframes spinner__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}.spinner__circle{position:absolute;width:100%;height:100%;border:3px solid transparent;border-radius:100%;-webkit-animation-name:spinner__rotate;animation-name:spinner__rotate;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.spinner__circle--primary{border-right-color:#73fac8;-webkit-animation-duration:.5s;animation-duration:.5s}.spinner__circle--white{border-right-color:#fff;-webkit-animation-duration:.8s;animation-duration:.8s}.spinner__circle--dimmed{border-color:hsla(0,0%,100%,.05)}.updater{position:relative;width:1em;height:1em;border-radius:100%;background:rgba(69,74,74,.86)}@-webkit-keyframes updater__zoom{0%{transform:scale(.2);opacity:0}20%{opacity:.6}80%{transform:scale(.9);opacity:0}to{transform:scale(.9);opacity:0}}@keyframes updater__zoom{0%{transform:scale(.2);opacity:0}20%{opacity:.6}80%{transform:scale(.9);opacity:0}to{transform:scale(.9);opacity:0}}.updater__circle{position:absolute;width:100%;height:100%;background:#fff;border-radius:inherit;-webkit-animation-name:updater__zoom;animation-name:updater__zoom;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-duration:2s;animation-duration:2s}.loader{position:relative;width:1em;height:1em;border-radius:100%;background:rgba(69,74,74,.86)}@-webkit-keyframes loader__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}@keyframes loader__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}.loader__circle{position:absolute;width:100%;height:100%;border-radius:inherit;border:2px solid transparent;border-right-color:#fff;-webkit-animation-name:loader__rotate;animation-name:loader__rotate;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-duration:.5s;animation-duration:.5s;opacity:.6}.message{margin-bottom:1rem;padding:.6rem;border:2px solid transparent;border-radius:8px;color:#fff;font-size:.8em}.message--success{background:rgba(0,255,0,.1)}.message--warning{background:rgba(255,255,0,.1)}.message--error{background:rgba(255,0,0,.15)}.header{position:relative;display:flex;flex-direction:column;align-items:center;border-bottom:1px solid rgba(0,0,0,.3)}.header:after,.header:before{content:"";position:absolute;top:0;bottom:0;width:2rem;pointer-events:none}.header:before{left:0;background:linear-gradient(90deg,#282d2d,rgba(40,45,45,0))}.header:after{right:0;background:linear-gradient(90deg,rgba(40,45,45,0),#282d2d)}@-webkit-keyframes header__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}@keyframes header__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}.header__logo{position:relative;margin:1rem 0 .5rem;width:42px;height:42px;background:linear-gradient(135deg,#73fac8,#00bee1);border-radius:100%}.header__spinner{position:absolute;top:-4px;left:-4px;width:calc(100% + 8px);height:calc(100% + 8px);border-radius:100%;border:2px solid transparent;opacity:0;transition:opacity 1.2s ease;-webkit-animation-name:header__rotate;animation-name:header__rotate;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.header__spinner--visible{opacity:1}.header__spinner--white{border-right-color:#fff;-webkit-animation-duration:.5s;animation-duration:.5s}.header__spinner--primary{border-right-color:#73fac8;-webkit-animation-duration:1.2s;animation-duration:1.2s}.header__nav{display:flex;width:100%;overflow:auto;-webkit-overflow-scrolling:touch}.header__buttons{display:flex;margin:0 auto;padding:0 .5rem}.header__button{display:flex;align-items:center;margin:0 .5rem;padding:.6666666667rem 2px;outline:none;white-space:nowrap;transition:box-shadow .3s ease}.header__button.hovered,.header__button:focus,.header__button:hover{box-shadow:inset 0 -2px 0 hsla(0,0%,100%,.5)}.header__button.active{box-shadow:inset 0 -2px 0 #73fac8}.header__arrow{margin-left:.5rem;width:12px;height:12px;fill:currentColor}.content{--columns:2;display:grid;grid-template-columns:repeat(var(--columns),minmax(0,1fr));gap:1rem;margin:0 auto;padding:6vh 1rem;width:100%;max-width:1200px}@media (max-width:800px){.content{--columns:1}}.content__spacer{height:3vh;grid-column:1/-1}.barChart{position:relative;display:grid;grid-auto-flow:column;grid-template-columns:-webkit-min-content auto;grid-template-columns:min-content auto;gap:.5rem;padding-top:3.5rem;height:300px;overflow:hidden}.barChart__axis{display:flex;flex-direction:column;justify-content:space-between;align-self:stretch;min-width:40px}.barChart__row{position:relative;font-size:.8em;padding:.5rem 0}.barChart__row--top{transform:translateY(-100%)}.barChart__row--middle{transform:translateY(-50%)}.barChart__row--bottom{transform:translateY(0)}.barChart__row:after{content:"";position:absolute;left:0;bottom:0;width:1200px;height:1px;background:hsla(0,0%,100%,.05)}.barChart__columns{display:flex;flex-direction:row-reverse}.barChart__column{display:flex;align-items:flex-end;padding:0 .25rem;width:100%}.barChart__bar{position:relative;width:100%;height:var(--size);min-height:2px;background:#6e7373;transition:height .3s ease}.barChart__column.active .barChart__bar{background:#73fac8}.barChart__column.active .barChart__bar:after{--arrow-width:15px;--arrow-height:10px;content:attr(data-label);position:absolute;right:0;bottom:calc(100% + 1rem);padding:.2em .5em calc(.2em + var(--arrow-height));background:#fff;-webkit-clip-path:polygon(0 0,100% 0,100% 100%,calc(100% - var(--arrow-width)) calc(100% - var(--arrow-height)),0 calc(100% - var(--arrow-height)));clip-path:polygon(0 0,100% 0,100% 100%,calc(100% - var(--arrow-width)) calc(100% - var(--arrow-height)),0 calc(100% - var(--arrow-height)));color:currentColor;z-index:1}.line{margin:0 0 1rem;height:1px;background:hsla(0,0%,100%,.05);border:0}.linkItem{display:flex;justify-content:space-between;align-items:center;margin:0 0 1rem;width:100%;transition:color .3s ease;outline:none}.linkItem--disabled{cursor:inherit}.linkItem:not(.linkItem--disabled):focus,.linkItem:not(.linkItem--disabled):hover{color:#fff}@media (max-width:800px){.linkItem{flex-direction:column;align-items:flex-start;text-align:left}}.flexList{position:relative}.flexList:after,.flexList:before{content:"";position:absolute;left:0;right:0;height:.5rem;pointer-events:none;z-index:1}.flexList:before{top:0;background:linear-gradient(#333838,rgba(51,56,56,0))}.flexList:after{bottom:0;background:linear-gradient(rgba(51,56,56,0),#333838)}.flexList__inner{padding-top:1rem;height:300px;overflow:auto;-webkit-overflow-scrolling:touch}.flexList__row{position:relative;display:flex;align-items:center;height:calc(32px + 1rem);border-bottom:1px solid hsla(0,0%,100%,.05);font-size:.9em}.flexList__row:last-child{border-bottom:none}.flexList__row--has-hover,.flexList__row[href]{color:currentColor;text-decoration:none;transition:color .3s ease}.flexList__row--has-hover:hover,.flexList__row[href]:hover{color:#fff}.flexList__column{display:grid;grid-auto-flow:column}.flexList__column--fixed-width{width:var(--width);flex-shrink:0}.flexList__column--spacing-left{margin-left:1rem}.flexList__column--spacing-right{margin-right:1rem}.flexList__column--text-adjustment{margin-top:3px}.flexList__bar{position:absolute;left:0;top:.5rem;width:var(--width);height:32px;border-radius:100px;background:hsla(0,0%,100%,.05);pointer-events:none}.flexList__bar--favicon{min-width:32px}.flexList__bar--counter{min-width:52px}.flexList__obscured,.flexList__truncated{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.flexList__obscured{opacity:0;transition:opacity .3s ease}.flexList__row:hover .flexList__obscured{opacity:.6}.emptyState{display:grid;align-content:center;justify-content:center;height:300px}.emptyState__inner{display:grid;align-items:center;grid-auto-flow:column;gap:.6666666667rem}.emptyState__icon{fill:hsla(0,0%,100%,.5);width:24px;height:24px}.valueText{display:grid;grid-auto-flow:column;justify-content:start;align-items:end;gap:.5rem}.favicon{width:32px;height:32px;background:#fff;border-radius:100%;border:2px solid #fff;overflow:hidden}.favicon--missing{background:linear-gradient(135deg,#73fac8,#00bee1)}.modal{position:fixed;display:flex;align-items:center;justify-content:center;top:0;right:0;bottom:0;left:0;padding:0 1rem;background:rgba(26,29,29,.9);pointer-events:none;opacity:0;z-index:3;transition:opacity .3s ease}.modal.visible{opacity:1;pointer-events:all}.modal__inner{width:100%;max-width:600px;max-height:100%;overflow:auto;-webkit-overflow-scrolling:touch;-ms-scroll-chaining:none;overscroll-behavior:contain;transform:translateY(20px);transition:transform .3s ease}.modal.visible .modal__inner{transform:none}.context{position:absolute;display:grid;top:0;left:0;padding:.5rem 0;min-width:210px;background:rgba(69,74,74,.98);border-radius:10px;transform:translate(var(--x),var(--y));z-index:3;opacity:0;pointer-events:none}@supports ((-webkit-backdrop-filter:blur(10px)) or (backdrop-filter:blur(10px))){.context{background:rgba(69,74,74,.86);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}}@media (max-width:800px){.context{font-size:.9em}}.context--floating{position:fixed;min-width:210px;border-radius:12px}.context.visible{opacity:1;pointer-events:all}.context__button{padding:.3333333333rem 1.2rem;color:currentColor;text-align:left;transition:color .3s ease,background .2s ease;line-height:1.4;outline:none}.context__button.active{color:#fff}.context__button:focus,.context__button:hover{background:hsla(0,0%,100%,.05)}.context--floating .context__button{margin:0 .5rem;padding:.5rem .625rem;color:#fff;border-radius:8px}.context--floating .context__button.active{color:#73fac8}.context__head{display:grid;grid-auto-flow:column;gap:1rem;align-items:center;justify-content:space-between}.context__label{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.context__description{padding-right:1rem;font-size:.86em;color:hsla(0,0%,100%,.5)}.context__separator{height:1px;background:hsla(0,0%,100%,.05);margin:.5rem 0}.filter{position:fixed;display:grid;justify-content:center;width:100%;bottom:4vh;z-index:2;pointer-events:none}@media (max-width:800px){.filter{font-size:.9em}}.filter__bar{display:grid;grid-auto-flow:column;gap:1rem;padding:0 1.5rem;border-radius:100px;background:rgba(69,74,74,.98);pointer-events:all}@supports ((-webkit-backdrop-filter:blur(10px)) or (backdrop-filter:blur(10px))){.filter__bar{background:rgba(69,74,74,.86);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}}.filter__button{display:flex;align-items:center;padding:.6666666667rem 0;white-space:nowrap;color:#fff;outline:none}.filter__button:focus{color:#73fac8}.filter__arrow{margin-left:.5rem;width:12px;height:12px;fill:hsla(0,0%,100%,.5)}.keyHint{display:flex;justify-content:center;width:1.5em;height:1.5em;font-size:.8em;background:hsla(0,0%,100%,.05);border-radius:3px;border:1px solid #6e7373;color:hsla(0,0%,100%,.5)}.keyHint,.status{align-items:center}.status{display:grid;gap:.5rem;grid-auto-flow:column;justify-content:start}.facts{--columns:3;grid-column:1/-1;display:grid;grid-template-columns:repeat(var(--columns),minmax(0,1fr));gap:1px;border-radius:10px;width:100%;overflow:hidden}@media (max-width:900px){.facts{--columns:2}}@media (max-width:560px){.facts{--columns:1}}.facts__card{display:grid;align-content:space-between;gap:1rem;background:#333838;padding:1.6rem 1.5rem 1.5rem;width:100%}.align-left{text-align:left}.align-right{text-align:right}.align-center{text-align:center}.spacer{height:calc(1rem*var(--size))}.color-primary{color:#73fac8}.color-white{color:#fff}.color-light{color:hsla(0,0%,100%,.5)}.color-black{color:#282d2d}.color-destructive{color:#ff3c3c} \ No newline at end of file diff --git a/dist/index.html b/dist/index.html deleted file mode 100644 index 6d0a4b91..00000000 --- a/dist/index.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - Ackee - - - - - - - - - - - - - - - - -
- - - \ No newline at end of file diff --git a/dist/tracker.js b/dist/tracker.js deleted file mode 100644 index 3cdb73d5..00000000 --- a/dist/tracker.js +++ /dev/null @@ -1 +0,0 @@ -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).ackeeTracker=e()}}((function(){return function e(t,n,r){function i(a,l){if(!n[a]){if(!t[a]){var s="function"==typeof require&&require;if(!l&&s)return s(a,!0);if(o)return o(a,!0);var u=new Error("Cannot find module '"+a+"'");throw u.code="MODULE_NOT_FOUND",u}var c=n[a]={exports:{}};t[a][0].call(c.exports,(function(e){return i(t[a][1][e]||e)}),c,c.exports,e,t,n,r)}return n[a].exports}for(var o="function"==typeof require&&require,a=0;a-1&&r<=s)for(;++n3?"WebKit":/\bOpera\b/.test(N)&&(/\bOPR\b/.test(t)?"Blink":"Presto"))||/\b(?:Midori|Nook|Safari)\b/i.test(t)&&!/^(?:Trident|EdgeHTML)$/.test(G)&&"WebKit"||!G&&/\bMSIE\b/i.test(t)&&("Mac OS"==K?"Tasman":"Trident")||"WebKit"==G&&/\bPlayStation\b(?! Vita\b)/i.test(N)&&"NetFront")&&(G=[l]),"IE"==N&&(l=(/; *(?:XBLWP|ZuneWP)(\d+)/i.exec(t)||0)[1])?(N+=" Mobile",K="Windows Phone "+(/\+$/.test(l)?l:l+".x"),$.unshift("desktop mode")):/\bWPDesktop\b/i.test(t)?(N="IE Mobile",K="Windows Phone 8.x",$.unshift("desktop mode"),j||(j=(/\brv:([\d.]+)/.exec(t)||0)[1])):"IE"!=N&&"Trident"==G&&(l=/\brv:([\d.]+)/.exec(t))&&(N&&$.push("identifying as "+N+(j?" "+j:"")),N="IE",j=l[1]),F){if(d="global",p=null!=(c=n)?typeof c[d]:"number",/^(?:boolean|number|string|undefined)$/.test(p)||"object"==p&&!c[d])g(l=n.runtime)==w?(N="Adobe AIR",K=l.flash.system.Capabilities.os):g(l=n.phantom)==M?(N="PhantomJS",j=(l=l.version||null)&&l.major+"."+l.minor+"."+l.patch):"number"==typeof k.documentMode&&(l=/\bTrident\/(\d+)/i.exec(t))?(j=[j,k.documentMode],(l=+l[1]+4)!=j[1]&&($.push("IE "+j[1]+" mode"),G&&(G[1]=""),j[1]=l),j="IE"==N?String(j[1].toFixed(1)):j[0]):"number"==typeof k.documentMode&&/^(?:Chrome|Firefox)\b/.test(N)&&($.push("masking as "+N+" "+j),N="IE",j="11.0",G=["Trident"],K="Windows");else if(P&&(W=(l=P.lang.System).getProperty("os.arch"),K=K||l.getProperty("os.name")+" "+l.getProperty("os.version")),I){try{j=n.require("ringo/engine").version.join("."),N="RingoJS"}catch(e){(l=n.system)&&l.global.system==n.system&&(N="Narwhal",K||(K=l[0].os||null))}N||(N="Rhino")}else"object"==typeof n.process&&!n.process.browser&&(l=n.process)&&("object"==typeof l.versions&&("string"==typeof l.versions.electron?($.push("Node "+l.versions.node),N="Electron",j=l.versions.electron):"string"==typeof l.versions.nw&&($.push("Chromium "+j,"Node "+l.versions.node),N="NW.js",j=l.versions.nw)),N||(N="Node.js",W=l.arch,K=l.platform,j=(j=/[\d.]+/.exec(l.version))?j[0]:null));K=K&&f(K)}if(j&&(l=/(?:[ab]|dp|pre|[ab]\d+pre)(?:\d+\+?)?$/i.exec(j)||/(?:alpha|beta)(?: ?\d)?/i.exec(t+";"+(F&&o.appMinorVersion))||/\bMinefield\b/i.test(t)&&"a")&&(T=/b/i.test(l)?"beta":"alpha",j=j.replace(RegExp(l+"\\+?$"),"")+("beta"==T?C:A)+(/\d+\+?/.exec(l)||"")),"Fennec"==N||"Firefox"==N&&/\b(?:Android|Firefox OS|KaiOS)\b/.test(K))N="Firefox Mobile";else if("Maxthon"==N&&j)j=j.replace(/\.[\d.]+/,".x");else if(/\bXbox\b/i.test(X))"Xbox 360"==X&&(K=null),"Xbox 360"==X&&/\bIEMobile\b/.test(t)&&$.unshift("mobile mode");else if(!/^(?:Chrome|IE|Opera)$/.test(N)&&(!N||X||/Browser|Mobi/.test(N))||"Windows CE"!=K&&!/Mobi/i.test(t))if("IE"==N&&F)try{null===n.external&&$.unshift("platform preview")}catch(e){$.unshift("embedded")}else(/\bBlackBerry\b/.test(X)||/\bBB10\b/.test(t))&&(l=(RegExp(X.replace(/ +/g," *")+"/([.\\d]+)","i").exec(t)||0)[1]||j)?(K=((l=[l,/BB10/.test(t)])[1]?(X=null,D="BlackBerry"):"Device Software")+" "+l[0],j=null):this!=h&&"Wii"!=X&&(F&&R||/Opera/.test(N)&&/\b(?:MSIE|Firefox)\b/i.test(t)||"Firefox"==N&&/\bOS X (?:\d+\.){2,}/.test(K)||"IE"==N&&(K&&!/^Win/.test(K)&&j>5.5||/\bWindows XP\b/.test(K)&&j>8||8==j&&!/\bTrident\b/.test(t)))&&!u.test(l=e.call(h,t.replace(u,"")+";"))&&l.name&&(l="ing as "+l.name+((l=l.version)?" "+l:""),u.test(N)?(/\bIE\b/.test(l)&&"Mac OS"==K&&(K=null),l="identify"+l):(l="mask"+l,N=B?f(B.replace(/([a-z])([A-Z])/g,"$1 $2")):"Opera",/\bIE\b/.test(l)&&(K=null),F||(j=null)),G=["Presto"],$.push(l));else N+=" Mobile";(l=(/\bAppleWebKit\/([\d.]+\+?)/i.exec(t)||0)[1])&&(l=[parseFloat(l.replace(/\.(\d)$/,".0$1")),l],"Safari"==N&&"+"==l[1].slice(-1)?(N="WebKit Nightly",T="alpha",j=l[1].slice(0,-1)):j!=l[1]&&j!=(l[2]=(/\bSafari\/([\d.]+\+?)/i.exec(t)||0)[1])||(j=null),l[1]=(/\b(?:Headless)?Chrome\/([\d.]+)/i.exec(t)||0)[1],537.36==l[0]&&537.36==l[2]&&parseFloat(l[1])>=28&&"WebKit"==G&&(G=["Blink"]),F&&(y||l[1])?(G&&(G[1]="like Chrome"),l=l[1]||((l=l[0])<530?1:l<532?2:l<532.05?3:l<533?4:l<534.03?5:l<534.07?6:l<534.1?7:l<534.13?8:l<534.16?9:l<534.24?10:l<534.3?11:l<535.01?12:l<535.02?"13+":l<535.07?15:l<535.11?16:l<535.19?17:l<536.05?18:l<536.1?19:l<537.01?20:l<537.11?"21+":l<537.13?23:l<537.18?24:l<537.24?25:l<537.36?26:"Blink"!=G?"27":"28")):(G&&(G[1]="like Safari"),l=(l=l[0])<400?1:l<500?2:l<526?3:l<533?4:l<534?"4+":l<535?5:l<537?6:l<538?7:l<601?8:l<602?9:l<604?10:l<606?11:l<608?12:"12"),G&&(G[1]+=" "+(l+="number"==typeof l?".x":/[.+]/.test(l)?"":"+")),"Safari"==N&&(!j||parseInt(j)>45)?j=l:"Chrome"==N&&/\bHeadlessChrome/i.test(t)&&$.unshift("headless")),"Opera"==N&&(l=/\bzbov|zvav$/.exec(K))?(N+=" ",$.unshift("desktop mode"),"zvav"==l?(N+="Mini",j=null):N+="Mobile",K=K.replace(RegExp(" *"+l+"$"),"")):"Safari"==N&&/\bChrome\b/.exec(G&&G[1])?($.unshift("desktop mode"),N="Chrome Mobile",j=null,/\bOS X\b/.test(K)?(D="Apple",K="iOS 4.3+"):K=null):/\bSRWare Iron\b/.test(N)&&!j&&(j=_("Chrome")),j&&0==j.indexOf(l=/[\d.]+$/.exec(K))&&t.indexOf("/"+l+"-")>-1&&(K=x(K.replace(l,""))),K&&-1!=K.indexOf(N)&&!RegExp(N+" OS").test(K)&&(K=K.replace(RegExp(" *"+S(N)+" *"),"")),G&&!/\b(?:Avant|Nook)\b/.test(N)&&(/Browser|Lunascape|Maxthon/.test(N)||"Safari"!=N&&/^iOS/.test(K)&&/\bSafari\b/.test(G[1])||/^(?:Adobe|Arora|Breach|Midori|Opera|Phantom|Rekonq|Rock|Samsung Internet|Sleipnir|SRWare Iron|Vivaldi|Web)/.test(N)&&G[1])&&(l=G[G.length-1])&&$.push(l),$.length&&($=["("+$.join("; ")+")"]),D&&X&&X.indexOf(D)<0&&$.push("on "+D),X&&$.push((/^on /.test($[$.length-1])?"":"on ")+X),K&&(l=/ ([\d.+]+)$/.exec(K),s=l&&"/"==K.charAt(K.length-l[0].length-1),K={architecture:32,family:l&&!s?K.replace(l[0],""):K,version:l?l[1]:null,toString:function(){var e=this.version;return this.family+(e&&!s?" "+e:"")+(64==this.architecture?" 64-bit":"")}}),(l=/\b(?:AMD|IA|Win|WOW|x86_|x)64\b/i.exec(W))&&!/\bi686\b/i.test(W)?(K&&(K.architecture=64,K.family=K.family.replace(RegExp(" *"+l),"")),N&&(/\bWOW64\b/i.test(t)||F&&/\w(?:86|32)$/.test(o.cpuClass||o.platform)&&!/\bWin64; x64\b/i.test(t))&&$.unshift("32-bit")):K&&/^OS X/.test(K.family)&&"Chrome"==N&&parseFloat(j)>=39&&(K.architecture=64),t||(t=null);var H={};return H.description=t,H.layout=G&&G[0],H.manufacturer=D,H.name=N,H.prerelease=T,H.product=X,H.ua=t,H.version=N&&j,H.os=K||{architecture:null,family:null,version:null,toString:function(){return"null"}},H.parse=e,H.toString=function(){return this.description||""},H.version&&$.unshift(j),H.name&&$.unshift(N),K&&N&&(K!=String(K).split(" ")[0]||K!=N.split(" ")[0]&&!X)&&$.push(X?"("+K+")":"on "+K),$.length&&(H.description=$.join(" ")),H}();o&&a?h(y,(function(e,t){o[t]=e})):i.platform=y}).call(this)}).call(this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],2:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.create=n.detect=n.attributes=void 0;var r,i=(r=e("platform"))&&r.__esModule?r:{default:r};function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function a(e){for(var t=1;t0&&void 0!==arguments[0]&&arguments[0],t={siteLocation:window.location.href,siteReferrer:document.referrer},n={siteLanguage:(navigator.language||navigator.userLanguage).substr(0,2),screenWidth:screen.width,screenHeight:screen.height,screenColorDepth:screen.colorDepth,deviceName:i.default.product,deviceManufacturer:i.default.manufacturer,osName:i.default.os.family,osVersion:i.default.os.version,browserName:i.default.name,browserVersion:i.default.version,browserWidth:window.outerWidth,browserHeight:window.outerHeight};return a(a({},t),!0===e?n:{})};n.attributes=c;var d=function(e,t){return{query:"\n\t\t\tmutation createRecord($domainId: ID!, $input: CreateRecordInput!) {\n\t\t\t\tcreateRecord(domainId: $domainId, input: $input) {\n\t\t\t\t\tpayload {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t",variables:{domainId:e,input:t}}},b=function(e){return{query:"\n\t\t\tmutation updateRecord($recordId: ID!) {\n\t\t\t\tupdateRecord(id: $recordId) {\n\t\t\t\t\tsuccess\n\t\t\t\t}\n\t\t\t}\n\t\t",variables:{recordId:e}}},p=function(e,t,n){var r=new XMLHttpRequest;r.open("POST",e),r.onload=function(){if(200!==r.status)throw new Error("Server returned with an unhandled status");var e=null;try{e=JSON.parse(r.responseText)}catch(e){throw new Error("Failed to parse response from server")}if(null!=e.errors)throw new Error(e.errors[0].message);if("function"==typeof n)return n(e)},r.setRequestHeader("Content-Type","application/json;charset=UTF-8"),r.withCredentials=!0,r.send(JSON.stringify(t))},f=function(){var e=document.querySelector("[data-ackee-domain-id]");if(null!=e){var t=e.getAttribute("data-ackee-server")||"",n=e.getAttribute("data-ackee-domain-id"),r=e.getAttribute("data-ackee-opts")||"{}";h(t,JSON.parse(r)).record(n)}};n.detect=f;var h=function(e,t){t=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t={};return t.detailed=!0===e.detailed,t.ignoreLocalhost=!1!==e.ignoreLocalhost,t}(t);var n,r,i=function(e){var t="/"===e.substr(-1);return e+(!0===t?"":"/")+"api"}(e),o=function(){},a={record:function(){return{stop:o}},updateRecord:function(){return{stop:o}},action:o,updateAction:o};if(!0===t.ignoreLocalhost&&!0==(""===(n=location.hostname)||"localhost"===n||"127.0.0.1"===n||"::1"===n))return console.warn("Ackee ignores you because you are on localhost"),a;if(!0===(r=navigator.userAgent,/bot|crawler|spider|crawling/i.test(r)))return console.warn("Ackee ignores you because you are a bot"),a;return{record:function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:c(t.detailed),r=arguments.length>2?arguments[2]:void 0,o=!1,a=function(){o=!0};return p(i,d(e,n),(function(e){var t=e.data.createRecord.payload.id;if(!0===u(t))return console.warn("Ackee ignores you because this is your own site");var n=setInterval((function(){!0!==o?p(i,b(t)):clearInterval(n)}),15e3);return"function"==typeof r?r(t):void 0})),{stop:a}},updateRecord:function(e){var t=!1,n=function(){t=!0};if(!0===u(e))return console.warn("Ackee ignores you because this is your own site"),{stop:n};var r=setInterval((function(){!0!==t?p(i,b(e)):clearInterval(r)}),15e3);return{stop:n}},action:function(e,t,n){p(i,function(e,t){return{query:"\n\t\t\tmutation createAction($eventId: ID!, $input: CreateActionInput!) {\n\t\t\t\tcreateAction(eventId: $eventId, input: $input) {\n\t\t\t\t\tpayload {\n\t\t\t\t\t\tid\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t",variables:{eventId:e,input:t}}}(e,t),(function(e){var t=e.data.createAction.payload.id;return!0===u(t)?console.warn("Ackee ignores you because this is your own site"):"function"==typeof n?n(t):void 0}))},updateAction:function(e,t){if(!0===u(e))return console.warn("Ackee ignores you because this is your own site");p(i,function(e,t){return{query:"\n\t\t\tmutation updateAction($actionId: ID!, $input: UpdateActionInput!) {\n\t\t\t\tupdateAction(id: $actionId, input: $input) {\n\t\t\t\t\tsuccess\n\t\t\t\t}\n\t\t\t}\n\t\t",variables:{actionId:e,input:t}}}(e,t))}}};n.create=h,!0===s&&f()},{platform:1}]},{},[2])(2)})); \ No newline at end of file diff --git a/prebuild.js b/prebuild.js index d1059ca1..74847fd7 100755 --- a/prebuild.js +++ b/prebuild.js @@ -1,8 +1,9 @@ #!/usr/bin/env node 'use strict' -const { styles, tracker, build } = require('./src/ui/index') +const { styles, scripts, tracker, build } = require('./src/ui/index') // Build files that are identical on every installation build('dist/index.css', styles) +build('dist/index.js', scripts) build('dist/tracker.js', tracker) \ No newline at end of file diff --git a/src/ui/index.js b/src/ui/index.js index b7225ee5..ba4ec4a5 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -5,10 +5,20 @@ const { writeFile, readFile } = require('fs').promises const sass = require('rosid-handler-sass') const js = require('rosid-handler-js-next') +const layout = require('../utils/layout') const isDemoMode = require('../utils/isDemoMode') const isDevelopmentMode = require('../utils/isDevelopmentMode') const signale = require('../utils/signale') +const index = async () => { + + return layout('
', 'favicon.ico', [ 'index.css' ], [ 'index.js' ], { + isDemoMode, + isDevelopmentMode + }) + +} + const styles = async () => { const filePath = resolve(__dirname, './styles/index.scss') @@ -60,6 +70,7 @@ const build = async (path, fn) => { } module.exports = { + index, styles, scripts, tracker, diff --git a/src/utils/layout.js b/src/utils/layout.js new file mode 100644 index 00000000..db5aeb05 --- /dev/null +++ b/src/utils/layout.js @@ -0,0 +1,34 @@ +'use strict' + +module.exports = (body, favicon, styles, scripts, variables) => ` + + + + + Ackee + + + + + + + + + ${ styles.map((src) => ``).join('') } + + + ${ scripts.map((src) => ``).join('') } + + + + + + + + ${ body } + + + +` \ No newline at end of file From a061e9d2e4b209d886c1ccb8540ab82b51d0eea5 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 16:29:32 +0100 Subject: [PATCH 137/208] Prepare build changes --- .gitignore | 2 +- build.js | 11 +++++++-- package.json | 8 +++--- prebuild.js | 9 ------- yarn.lock | 70 +++++++++++++++++++++++++--------------------------- 5 files changed, 47 insertions(+), 53 deletions(-) delete mode 100755 prebuild.js diff --git a/.gitignore b/.gitignore index d29fdd2c..d7aa90ea 100644 --- a/.gitignore +++ b/.gitignore @@ -31,9 +31,9 @@ node_modules # Dist folder dist/* -!dist/index.html !dist/favicon.ico !dist/index.css +!dist/index.js !dist/tracker.js # Vercel diff --git a/build.js b/build.js index 1bcf7e7e..438008a4 100755 --- a/build.js +++ b/build.js @@ -3,8 +3,15 @@ require('dotenv').config() const customTracker = require('./src/utils/customTracker') -const { index, tracker, build } = require('./src/ui/index') +const { index, styles, scripts, tracker, build } = require('./src/ui/index') -// Build files that depend on environment variables of the installation +// Build files that are identical on every installation +if (process.env.BUILD_ENV === 'pre') { + build('dist/index.css', styles) + build('dist/index.js', scripts) + build('dist/tracker.js', tracker) +} + +// Build files that depend on environment variables build(`dist/index.html`, index) if (customTracker.exists === true) build(`dist/${ customTracker.path }`, tracker) \ No newline at end of file diff --git a/package.json b/package.json index 1012cc50..d496d624 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,9 @@ "scripts": { "start": "npm run build && npm run server", "start:dev": "NODE_ENV=development nodemon", - "build:pre": "node prebuild.js", + "build:pre": "BUILD_ENV=pre npm run build", "build": "node build.js", - "build:dev": "NODE_ENV=development npm run build", "server": "node src/index.js", - "server:dev": "NODE_ENV=development npm run server", "coveralls": "nyc report --reporter=text-lcov | coveralls", "test": "nyc ava", "lint": "eslint '{functions,src,test}/**/*.js'" @@ -70,8 +68,6 @@ "redux-devtools-extension": "^2.13.8", "redux-thunk": "^2.3.0", "request-ip": "^2.1.3", - "rosid-handler-js-next": "^1.0.0", - "rosid-handler-sass": "^8.0.0", "s-ago": "^2.2.0", "sanitize-filename": "^1.6.3", "shortid": "^2.2.16", @@ -92,6 +88,8 @@ "mongodb-memory-server": "^6.9.2", "nodemon": "^2.0.6", "nyc": "^15.1.0", + "rosid-handler-js-next": "^1.0.0", + "rosid-handler-sass": "^8.0.0", "test-listen": "^1.1.0" }, "ava": { diff --git a/prebuild.js b/prebuild.js deleted file mode 100755 index 74847fd7..00000000 --- a/prebuild.js +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env node -'use strict' - -const { styles, scripts, tracker, build } = require('./src/ui/index') - -// Build files that are identical on every installation -build('dist/index.css', styles) -build('dist/index.js', scripts) -build('dist/tracker.js', tracker) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8d5370b2..18f6eb59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2475,17 +2475,7 @@ braces@^3.0.1, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.0.0, browserslist@^4.12.0: - version "4.14.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.6.tgz#97702a9c212e0c6b6afefad913d3a1538e348457" - integrity sha512-zeFYcUo85ENhc/zxHbiIp0LGzzTrE2Pv2JhxvS7kpUb9Q9D38kUX6Bie7pGutJ/5iF5rOxE7CepAuWD56xJ33A== - dependencies: - caniuse-lite "^1.0.30001154" - electron-to-chromium "^1.3.585" - escalade "^3.1.1" - node-releases "^1.1.65" - -browserslist@^4.14.5, browserslist@^4.15.0: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.15.0: version "4.16.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.0.tgz#410277627500be3cb28a1bfe037586fbedf9488b" integrity sha512-/j6k8R0p3nxOC6kx5JGAxsnhc9ixaWJfYc+TNTzxg6+ARaESAvQGV7h0uNOB4t+pLQJZWzcrMxXOxjgsCj3dqQ== @@ -2638,12 +2628,7 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001154: - version "1.0.30001156" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001156.tgz#75c20937b6012fe2b02ab58b30d475bf0718de97" - integrity sha512-z7qztybA2eFZTB6Z3yvaQBIoJpQtsewRD74adw2UbRWwsRq3jIPvgrQGawBMbfafekQaD21FWuXNcywtTDGGCw== - -caniuse-lite@^1.0.30001165: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001165: version "1.0.30001170" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001170.tgz#0088bfecc6a14694969e391cc29d7eb6362ca6a7" integrity sha512-Dd4d/+0tsK0UNLrZs3CvNukqalnVTRrxb5mcQm8rHL49t7V5ZaTygwXkrq+FB+dVDf++4ri8eJnFEJAB8332PA== @@ -3064,7 +3049,7 @@ css-tree@1.0.0-alpha.37: mdn-data "2.0.4" source-map "^0.6.1" -css-tree@^1.0.0, css-tree@^1.0.0-alpha.28: +css-tree@^1.0.0-alpha.28: version "1.0.0" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0.tgz#21993fa270d742642a90409a2c0cb3ac0298adf6" integrity sha512-CdVYz/Yuqw0VdKhXPBIgi8DO3NicJVYZNWeX9XcIuSp9ZoFT5IcleVRW07O5rMjdcx1mb+MEJPknTTEW7DdsYw== @@ -3072,6 +3057,14 @@ css-tree@^1.0.0, css-tree@^1.0.0-alpha.28: mdn-data "2.0.12" source-map "^0.6.1" +css-tree@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.2.tgz#9ae393b5dafd7dae8a622475caec78d3d8fbd7b5" + integrity sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + css-what@^3.2.1: version "3.4.2" resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" @@ -3156,11 +3149,11 @@ cssnano@^4.1.10: postcss "^7.0.0" csso@^4.0.2: - version "4.1.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.1.0.tgz#1d31193efa99b87aa6bad6c0cef155e543d09e8b" - integrity sha512-h+6w/W1WqXaJA4tb1dk7r5tVbOm97MsKxzwnvOR04UQ6GILroryjMWu3pmCCtL2mLaEStQ0fZgeGiy99mo7iyg== + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== dependencies: - css-tree "^1.0.0" + css-tree "^1.1.2" csstype@^2.5.5: version "2.6.13" @@ -3405,9 +3398,9 @@ domelementtype@1: integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== domelementtype@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.2.tgz#f3b6e549201e46f588b59463dd77187131fe6971" - integrity sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA== + version "2.1.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" + integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== domutils@^1.7.0: version "1.7.0" @@ -3442,11 +3435,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -electron-to-chromium@^1.3.585: - version "1.3.591" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.591.tgz#a18892bf1acb93f7b6e4da402705d564bc235017" - integrity sha512-ol/0WzjL4NS4Kqy9VD6xXQON91xIihDT36sYCew/G/bnd1v0/4D+kahp26JauQhgFUjrdva3kRSo7URcUmQ+qw== - electron-to-chromium@^1.3.621: version "1.3.633" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.633.tgz#16dd5aec9de03894e8d14a1db4cda8a369b9b7fe" @@ -5315,6 +5303,11 @@ mdn-data@2.0.12: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.12.tgz#bbb658d08b38f574bbb88f7b83703defdcc46844" integrity sha512-ULbAlgzVb8IqZ0Hsxm6hHSlQl3Jckst2YEQS7fODu9ilNWy2LvcoSY7TRFIktABP2mdppBioc66va90T+NUs8Q== +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + mdn-data@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" @@ -5619,11 +5612,6 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" -node-releases@^1.1.65: - version "1.1.66" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.66.tgz#609bd0dc069381015cd982300bae51ab4f1b1814" - integrity sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg== - node-releases@^1.1.67: version "1.1.67" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.67.tgz#28ebfcccd0baa6aad8e8d4d8fe4cbc49ae239c12" @@ -5820,7 +5808,17 @@ object.getownpropertydescriptors@^2.1.0: define-properties "^1.1.3" es-abstract "^1.17.0-next.1" -object.values@^1.1.0, object.values@^1.1.1: +object.values@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731" + integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.18.0-next.1" + has "^1.0.3" + +object.values@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== From c07637f13776467d8eee81a02d3e0516e701c5c9 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 16:35:26 +0100 Subject: [PATCH 138/208] Make sass and js handler optional and require them just when in use --- src/ui/index.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/ui/index.js b/src/ui/index.js index ba4ec4a5..c782c633 100644 --- a/src/ui/index.js +++ b/src/ui/index.js @@ -2,8 +2,6 @@ const { resolve } = require('path') const { writeFile, readFile } = require('fs').promises -const sass = require('rosid-handler-sass') -const js = require('rosid-handler-js-next') const layout = require('../utils/layout') const isDemoMode = require('../utils/isDemoMode') @@ -21,18 +19,19 @@ const index = async () => { const styles = async () => { + const sass = require('rosid-handler-sass') const filePath = resolve(__dirname, './styles/index.scss') - const data = sass(filePath, { optimize: isDevelopmentMode === false }) - return data + return sass(filePath, { optimize: isDevelopmentMode === false }) } const scripts = async () => { + const js = require('rosid-handler-js-next') const filePath = resolve(__dirname, './scripts/index.js') - const data = js(filePath, { + return js(filePath, { optimize: isDevelopmentMode === false, replace: { 'process.env.ACKEE_TRACKER': JSON.stringify(process.env.ACKEE_TRACKER), @@ -42,16 +41,13 @@ const scripts = async () => { babel: false }) - return data - } const tracker = async () => { const filePath = require.resolve('ackee-tracker') - const data = readFile(filePath, 'utf8') - return data + return readFile(filePath, 'utf8') } From b39aa19814633b80147942fefaafe9c2df77f665 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 16:39:45 +0100 Subject: [PATCH 139/208] Remove old export --- src/utils/customTracker.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/customTracker.js b/src/utils/customTracker.js index 6262e584..ac2e320e 100644 --- a/src/utils/customTracker.js +++ b/src/utils/customTracker.js @@ -5,8 +5,6 @@ const sanitizeFilename = require('sanitize-filename') const name = process.env.ACKEE_TRACKER const exists = name != null && name !== '' -module.exports = exists === true ? `/${ encodeURIComponent(name) }.js` : undefined - module.exports = { exists, url: exists === true ? `/${ encodeURIComponent(name) }.js` : undefined, From 258739d7ca8d9a1eed6e303c1f7c990590365ede Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 16:55:18 +0100 Subject: [PATCH 140/208] Move all deps that are only used in the UI to devDeps --- package.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index d496d624..fbcde301 100644 --- a/package.json +++ b/package.json @@ -37,17 +37,13 @@ "apollo-server-lambda": "^2.19.1", "apollo-server-micro": "^2.19.1", "apollo-server-plugin-http-headers": "^0.1.4", - "classnames": "^2.2.6", "date-fns": "^2.16.1", "date-fns-tz": "^1.0.10", "debounce-promise": "^3.1.2", "dotenv": "^8.2.0", - "formbase": "^12.0.1", "graphql": "^15.4.0", "graphql-scalars": "^1.7.0", "graphql-tools": "^7.0.2", - "human-number": "^1.0.6", - "immer": "^8.0.0", "is-url": "^1.2.4", "micro": "^9.3.4", "microrouter": "^3.1.3", @@ -55,41 +51,45 @@ "node-fetch": "^2.6.1", "node-schedule": "^1.3.2", "normalize-url": "^5.0.0", - "normalize.css": "^8.0.1", - "prop-types": "^15.7.2", - "react": "^17.0.1", - "react-dom": "^17.0.1", - "react-error-boundary": "^3.1.0", - "react-fast-compare": "^3.2.0", - "react-hotkeys-hook": "^2.4.0", - "react-redux": "^7.2.2", - "react-use": "^15.3.4", - "redux": "^4.0.5", - "redux-devtools-extension": "^2.13.8", - "redux-thunk": "^2.3.0", "request-ip": "^2.1.3", - "s-ago": "^2.2.0", "sanitize-filename": "^1.6.3", - "shortid": "^2.2.16", "signale": "^1.4.0", "uuid": "^8.3.2" }, "devDependencies": { "@electerious/eslint-config": "^1.3.4", "ava": "3.14.0", + "classnames": "^2.2.6", "coveralls": "^3.1.0", "eslint": "^7.16.0", "eslint-plugin-import": "^2.22.0", "eslint-plugin-react": "^7.21.5", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-native": "^3.9.1", + "formbase": "^12.0.1", + "human-number": "^1.0.6", "husky": "^4.3.6", + "immer": "^8.0.0", "mocked-env": "^1.3.2", "mongodb-memory-server": "^6.9.2", "nodemon": "^2.0.6", + "normalize.css": "^8.0.1", "nyc": "^15.1.0", + "prop-types": "^15.7.2", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "react-error-boundary": "^3.1.0", + "react-fast-compare": "^3.2.0", + "react-hotkeys-hook": "^3.0.0", + "react-redux": "^7.2.2", + "react-use": "^15.3.4", + "redux": "^4.0.5", + "redux-devtools-extension": "^2.13.8", + "redux-thunk": "^2.3.0", "rosid-handler-js-next": "^1.0.0", "rosid-handler-sass": "^8.0.0", + "s-ago": "^2.2.0", + "shortid": "^2.2.16", "test-listen": "^1.1.0" }, "ava": { From e9d1d308f9798d0b45360bc832854f5032cb68e0 Mon Sep 17 00:00:00 2001 From: Tobias Reich Date: Sun, 27 Dec 2020 16:58:21 +0100 Subject: [PATCH 141/208] Only rebuild index.html when starting Ackee and commit all other build files --- .dockerignore | 5 +- Dockerfile | 8 ++-- dist/index.css | 1 + dist/index.js | 46 +++++++++++++++++++ dist/tracker.js | 1 + package.json | 2 +- src/ui/index.js | 4 +- .../components/modals/ModalDomainEdit.js | 3 +- .../components/overlays/OverlayLogin.js | 5 +- src/ui/scripts/index.js | 4 +- yarn.lock | 35 +++++--------- 11 files changed, 74 insertions(+), 40 deletions(-) create mode 100644 dist/index.css create mode 100644 dist/index.js create mode 100644 dist/tracker.js diff --git a/.dockerignore b/.dockerignore index e7620479..b0fd0ccf 100644 --- a/.dockerignore +++ b/.dockerignore @@ -31,7 +31,10 @@ node_modules # Dist folder dist/* -!dist/.gitkeep +!dist/favicon.ico +!dist/index.css +!dist/index.js +!dist/tracker.js # Files not required by Docker Dockerfile diff --git a/Dockerfile b/Dockerfile index 151cbb42..1e72bfca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,18 +3,16 @@ FROM mhart/alpine-node:14 AS build WORKDIR /srv/app/ -# Add dependencies first so that the docker image build can use -# the cache as long as the dependencies stay unchanged. +# Add dependencies first so that Docker can use the cache as long as the dependencies stay unchanged COPY package.json yarn.lock /srv/app/ RUN yarn install --production --frozen-lockfile -# Copy and compile the source after the dependency step as it's -# more likely that the source changes. +# Copy source after the dependency step as it's more likely that the source changes COPY build.js /srv/app/ COPY src /srv/app/src -RUN mkdir dist && yarn build +COPY dist /srv/app/dist # Start with second build stage diff --git a/dist/index.css b/dist/index.css new file mode 100644 index 00000000..df0755d2 --- /dev/null +++ b/dist/index.css @@ -0,0 +1 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}[hidden],template{display:none}.input{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;width:100%;margin:0 0 .9rem;padding:.6rem;background:hsla(0,0%,100%,.05);border:1px solid transparent;border-radius:8px;outline:none;resize:vertical;transition:border-color .3s ease;font-family:inherit;font-size:100%;line-height:1.15}.input,.input[disabled]{box-shadow:none;color:#fff}.input[disabled]{border-color:rgba(13,13,13,0);background-color:hsla(0,0%,94.9%,.05);cursor:not-allowed}.input:focus{border-color:#73fac8}.input::-moz-placeholder{color:#999;opacity:1}.input:-ms-input-placeholder{opacity:1}.input::placeholder{color:#999;opacity:1}.input:-ms-input-placeholder{color:#999}.input::-ms-input-placeholder{color:#999}.control{position:relative;margin:0 0 .9rem;box-sizing:border-box}.control__input{position:absolute;opacity:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin:0;left:0;top:calc(50% - 10px);width:20px;height:20px;pointer-events:none}.control__label{display:flex;align-items:center;position:relative;color:#fff}.control__label:after,.control__label:before{content:"";display:block;width:20px;height:20px;border:1px solid transparent}.control__label:before{flex-shrink:0;transition:border-color .3s ease;margin-right:.6rem;border-color:transparent;box-shadow:none;background:hsla(0,0%,100%,.05)}.control__label:after{position:absolute;top:calc(50% - 11px);left:0;background-size:60%;background-repeat:no-repeat;background-position:50%;transform:scale(0);transition:transform .3s ease}.control__input[type=radio]+.control__label:before{border-radius:100%}.control__input[type=radio]+.control__label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cpath fill='%23fff' d='M256 464c114.9 0 208-93.1 208-208S370.9 48 256 48 48 141.1 48 256s93.1 208 208 208z'/%3E%3C/svg%3E")}.control__input[type=checkbox]+.control__label:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cpath fill='%23fff' d='M461.6 109.6l-54.9-43.3c-1.7-1.4-3.8-2.4-6.2-2.4-2.4 0-4.6 1-6.3 2.5L194.5 323s-78.5-75.5-80.7-77.7c-2.2-2.2-5.1-5.9-9.5-5.9s-6.4 3.1-8.7 5.4c-1.7 1.8-29.7 31.2-43.5 45.8-.8.9-1.3 1.4-2 2.1-1.2 1.7-2 3.6-2 5.7 0 2.2.8 4 2 5.7l2.8 2.6s139.3 133.8 141.6 136.1c2.3 2.3 5.1 5.2 9.2 5.2 4 0 7.3-4.3 9.2-6.2l249.1-320c1.2-1.7 2-3.6 2-5.8 0-2.5-1-4.6-2.4-6.4z'/%3E%3C/svg%3E")}.control__input[type=checkbox][disabled]+.control__label,.control__input[type=radio][disabled]+.control__label{cursor:not-allowed;color:#fff}.control__input[type=checkbox][disabled]+.control__label:before,.control__input[type=radio][disabled]+.control__label:before{border-color:rgba(13,13,13,0);background-color:hsla(0,0%,94.9%,.05);box-shadow:none}.control__input:focus+.control__label:before{border-color:#73fac8}.control__input:checked+.control__label:after{transform:scale(1)}.select{-webkit-appearance:none;-moz-appearance:none;appearance:none;box-sizing:border-box;margin:0 0 .9rem;padding:.6rem calc(.9rem + 12px) .6rem .6rem;width:100%;background:hsla(0,0%,100%,.05);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512'%3E%3Cpath fill='%23fff' d='M256 298.3l174.2-167.2c4.3-4.2 11.4-4.1 15.8.2l30.6 29.9c4.4 4.3 4.5 11.3.2 15.5L264.1 380.9c-2.2 2.2-5.2 3.2-8.1 3-3 .1-5.9-.9-8.1-3L35.2 176.7c-4.3-4.2-4.2-11.2.2-15.5L66 131.3c4.4-4.3 11.5-4.4 15.8-.2L256 298.3z'/%3E%3C/svg%3E");background-size:12px;background-repeat:no-repeat;background-position:calc(100% - .6rem) 50%;border:1px solid transparent;border-radius:8px;outline:0;transition:border-color .3s ease;font-family:inherit;font-size:100%;line-height:1.15}.select,.select[disabled]{box-shadow:none;color:#fff}.select[disabled]{border-color:rgba(13,13,13,0);background-color:hsla(0,0%,94.9%,.05);cursor:not-allowed}.select:focus{border-color:#73fac8}.select::-ms-expand{display:none}html{width:100%;height:100%;font:normal 400 112.5%/1.5 Rubik,sans-serif}body{display:grid;color:hsla(0,0%,100%,.5);background:#282d2d;grid-template-columns:100%;min-height:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body *,body :after,body :before{box-sizing:border-box}.customScrollbar ::-webkit-scrollbar{width:6px;height:6px;z-index:1000}.customScrollbar ::-webkit-scrollbar-track{background:transparent}.customScrollbar ::-webkit-scrollbar-thumb{background-color:hsla(0,0%,100%,.5);border-radius:3px}.customScrollbar ::-webkit-scrollbar-corner{display:none}.customScrollbar *{scrollbar-width:thin;scrollbar-color:hsla(0,0%,100%,.5) transparent}@font-face{font-family:Rubik;src:url("data:application/font-woff2;charset=utf-8;base64,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") format("woff2");font-style:normal;font-weight:400}.headline{margin:0 0 .5rem;color:#fff;font-weight:400;font-size:2.2em;text-rendering:optimizeLegibility;line-height:1}.headline--medium{margin:0 0 .25rem;font-size:1.5em;line-height:1.2}.headline--small{margin:0 0 .3333333333rem;font-size:inherit;line-height:inherit}.headline--no-spacing{margin-bottom:0}.headline__button{text-align:left;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.headline__button:focus{color:#73fac8;outline:none}.text{margin:0 0 1rem}.text--no-spacing{margin-bottom:0}.label{display:block;font-size:.9em;margin:0 0 .25rem}.link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;padding:0;border:none;color:currentColor;font:inherit;border-radius:0;cursor:pointer;text-decoration:none}.input,.select{border-width:2px}.select{padding-right:2rem}.select option{color:#000}.select:disabled{opacity:.6;background-image:none}.control__label{padding-left:calc(24px + .6rem)}.control__label:after,.control__label:before{top:calc(50% - 12px);width:24px;height:24px}.control__label:before{border-width:2px}.inputMessage{display:grid;grid-template-areas:"main";position:relative;margin-bottom:.9rem}@-webkit-keyframes inputMessage__show{0%{opacity:1}60%{opacity:1}to{opacity:0}}@keyframes inputMessage__show{0%{opacity:1}60%{opacity:1}to{opacity:0}}.inputMessage .input{grid-area:main;margin-bottom:0}.inputMessage:after{content:attr(title);grid-area:main;display:flex;align-items:center;justify-content:center;background:rgba(69,74,74,.98);border:2px solid #73fac8;border-radius:8px;color:#fff;font-size:.86em;opacity:0;pointer-events:none}@supports ((-webkit-backdrop-filter:blur(10px)) or (backdrop-filter:blur(10px))){.inputMessage:after{background:rgba(69,74,74,.86);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}}.inputMessage:focus-within:after{-webkit-animation-name:inputMessage__show;animation-name:inputMessage__show;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-duration:2s;animation-duration:2s}#main{display:grid;grid-template-columns:100%}.card{background:#333838;border-radius:10px;width:100%;overflow:hidden}.card--overlay{justify-self:center;align-self:center;max-width:420px}.card--wide{grid-column:1/-1}.card__inner{padding:1.6rem 1.5rem 1.5rem;width:100%}.card__footer{display:flex;border-top:1px solid #282d2d}.card__button{display:flex;justify-content:flex-end;padding:1rem 1.5rem;transition:background .3s ease;outline:none}.card__button[disabled]{cursor:not-allowed}.card__button:not([disabled]):focus,.card__button:not([disabled]):hover{background:hsla(0,0%,100%,.05)}.card__button:active{background:none;transition:none}.card__button--primary{width:100%}.card__separator{flex-shrink:0;width:1px;background:#282d2d}.spinner{position:relative;height:26px;width:26px}@-webkit-keyframes spinner__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}@keyframes spinner__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}.spinner__circle{position:absolute;width:100%;height:100%;border:3px solid transparent;border-radius:100%;-webkit-animation-name:spinner__rotate;animation-name:spinner__rotate;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.spinner__circle--primary{border-right-color:#73fac8;-webkit-animation-duration:.5s;animation-duration:.5s}.spinner__circle--white{border-right-color:#fff;-webkit-animation-duration:.8s;animation-duration:.8s}.spinner__circle--dimmed{border-color:hsla(0,0%,100%,.05)}.updater{position:relative;width:1em;height:1em;border-radius:100%;background:rgba(69,74,74,.86)}@-webkit-keyframes updater__zoom{0%{transform:scale(.2);opacity:0}20%{opacity:.6}80%{transform:scale(.9);opacity:0}to{transform:scale(.9);opacity:0}}@keyframes updater__zoom{0%{transform:scale(.2);opacity:0}20%{opacity:.6}80%{transform:scale(.9);opacity:0}to{transform:scale(.9);opacity:0}}.updater__circle{position:absolute;width:100%;height:100%;background:#fff;border-radius:inherit;-webkit-animation-name:updater__zoom;animation-name:updater__zoom;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-duration:2s;animation-duration:2s}.loader{position:relative;width:1em;height:1em;border-radius:100%;background:rgba(69,74,74,.86)}@-webkit-keyframes loader__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}@keyframes loader__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}.loader__circle{position:absolute;width:100%;height:100%;border-radius:inherit;border:2px solid transparent;border-right-color:#fff;-webkit-animation-name:loader__rotate;animation-name:loader__rotate;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-duration:.5s;animation-duration:.5s;opacity:.6}.message{margin-bottom:1rem;padding:.6rem;border:2px solid transparent;border-radius:8px;color:#fff;font-size:.8em}.message--success{background:rgba(0,255,0,.1)}.message--warning{background:rgba(255,255,0,.1)}.message--error{background:rgba(255,0,0,.15)}.header{position:relative;display:flex;flex-direction:column;align-items:center;border-bottom:1px solid rgba(0,0,0,.3)}.header:after,.header:before{content:"";position:absolute;top:0;bottom:0;width:2rem;pointer-events:none}.header:before{left:0;background:linear-gradient(90deg,#282d2d,rgba(40,45,45,0))}.header:after{right:0;background:linear-gradient(90deg,rgba(40,45,45,0),#282d2d)}@-webkit-keyframes header__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}@keyframes header__rotate{0%{transform:rotate(0turn)}to{transform:rotate(1turn)}}.header__logo{position:relative;margin:1rem 0 .5rem;width:42px;height:42px;background:linear-gradient(135deg,#73fac8,#00bee1);border-radius:100%}.header__spinner{position:absolute;top:-4px;left:-4px;width:calc(100% + 8px);height:calc(100% + 8px);border-radius:100%;border:2px solid transparent;opacity:0;transition:opacity 1.2s ease;-webkit-animation-name:header__rotate;animation-name:header__rotate;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.header__spinner--visible{opacity:1}.header__spinner--white{border-right-color:#fff;-webkit-animation-duration:.5s;animation-duration:.5s}.header__spinner--primary{border-right-color:#73fac8;-webkit-animation-duration:1.2s;animation-duration:1.2s}.header__nav{display:flex;width:100%;overflow:auto;-webkit-overflow-scrolling:touch}.header__buttons{display:flex;margin:0 auto;padding:0 .5rem}.header__button{display:flex;align-items:center;margin:0 .5rem;padding:.6666666667rem 2px;outline:none;white-space:nowrap;transition:box-shadow .3s ease}.header__button.hovered,.header__button:focus,.header__button:hover{box-shadow:inset 0 -2px 0 hsla(0,0%,100%,.5)}.header__button.active{box-shadow:inset 0 -2px 0 #73fac8}.header__arrow{margin-left:.5rem;width:12px;height:12px;fill:currentColor}.content{--columns:2;display:grid;grid-template-columns:repeat(var(--columns),minmax(0,1fr));gap:1rem;margin:0 auto;padding:6vh 1rem;width:100%;max-width:1200px}@media (max-width:800px){.content{--columns:1}}.content__spacer{height:3vh;grid-column:1/-1}.barChart{position:relative;display:grid;grid-auto-flow:column;grid-template-columns:-webkit-min-content auto;grid-template-columns:min-content auto;gap:.5rem;padding-top:3.5rem;height:300px;overflow:hidden}.barChart__axis{display:flex;flex-direction:column;justify-content:space-between;align-self:stretch;min-width:40px}.barChart__row{position:relative;font-size:.8em;padding:.5rem 0}.barChart__row--top{transform:translateY(-100%)}.barChart__row--middle{transform:translateY(-50%)}.barChart__row--bottom{transform:translateY(0)}.barChart__row:after{content:"";position:absolute;left:0;bottom:0;width:1200px;height:1px;background:hsla(0,0%,100%,.05)}.barChart__columns{display:flex;flex-direction:row-reverse}.barChart__column{display:flex;align-items:flex-end;padding:0 .25rem;width:100%}.barChart__bar{position:relative;width:100%;height:var(--size);min-height:2px;background:#6e7373;transition:height .3s ease}.barChart__column.active .barChart__bar{background:#73fac8}.barChart__column.active .barChart__bar:after{--arrow-width:15px;--arrow-height:10px;content:attr(data-label);position:absolute;right:0;bottom:calc(100% + 1rem);padding:.2em .5em calc(.2em + var(--arrow-height));background:#fff;-webkit-clip-path:polygon(0 0,100% 0,100% 100%,calc(100% - var(--arrow-width)) calc(100% - var(--arrow-height)),0 calc(100% - var(--arrow-height)));clip-path:polygon(0 0,100% 0,100% 100%,calc(100% - var(--arrow-width)) calc(100% - var(--arrow-height)),0 calc(100% - var(--arrow-height)));color:currentColor;z-index:1}.line{margin:0 0 1rem;height:1px;background:hsla(0,0%,100%,.05);border:0}.linkItem{display:flex;justify-content:space-between;align-items:center;margin:0 0 1rem;width:100%;transition:color .3s ease;outline:none}.linkItem--disabled{cursor:inherit}.linkItem:not(.linkItem--disabled):focus,.linkItem:not(.linkItem--disabled):hover{color:#fff}@media (max-width:800px){.linkItem{flex-direction:column;align-items:flex-start;text-align:left}}.flexList{position:relative}.flexList:after,.flexList:before{content:"";position:absolute;left:0;right:0;height:.5rem;pointer-events:none;z-index:1}.flexList:before{top:0;background:linear-gradient(#333838,rgba(51,56,56,0))}.flexList:after{bottom:0;background:linear-gradient(rgba(51,56,56,0),#333838)}.flexList__inner{padding-top:1rem;height:300px;overflow:auto;-webkit-overflow-scrolling:touch}.flexList__row{position:relative;display:flex;align-items:center;height:calc(32px + 1rem);border-bottom:1px solid hsla(0,0%,100%,.05);font-size:.9em}.flexList__row:last-child{border-bottom:none}.flexList__row--has-hover,.flexList__row[href]{color:currentColor;text-decoration:none;transition:color .3s ease}.flexList__row--has-hover:hover,.flexList__row[href]:hover{color:#fff}.flexList__column{display:grid;grid-auto-flow:column}.flexList__column--fixed-width{width:var(--width);flex-shrink:0}.flexList__column--spacing-left{margin-left:1rem}.flexList__column--spacing-right{margin-right:1rem}.flexList__column--text-adjustment{margin-top:3px}.flexList__bar{position:absolute;left:0;top:.5rem;width:var(--width);height:32px;border-radius:100px;background:hsla(0,0%,100%,.05);pointer-events:none}.flexList__bar--favicon{min-width:32px}.flexList__bar--counter{min-width:52px}.flexList__obscured,.flexList__truncated{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.flexList__obscured{opacity:0;transition:opacity .3s ease}.flexList__row:hover .flexList__obscured{opacity:.6}.emptyState{display:grid;align-content:center;justify-content:center;height:300px}.emptyState__inner{display:grid;align-items:center;grid-auto-flow:column;gap:.6666666667rem}.emptyState__icon{fill:hsla(0,0%,100%,.5);width:24px;height:24px}.valueText{display:grid;grid-auto-flow:column;justify-content:start;align-items:end;gap:.5rem}.favicon{width:32px;height:32px;background:#fff;border-radius:100%;border:2px solid #fff;overflow:hidden}.favicon--missing{background:linear-gradient(135deg,#73fac8,#00bee1)}.modal{position:fixed;display:flex;align-items:center;justify-content:center;top:0;right:0;bottom:0;left:0;padding:0 1rem;background:rgba(26,29,29,.9);pointer-events:none;opacity:0;z-index:3;transition:opacity .3s ease}.modal.visible{opacity:1;pointer-events:all}.modal__inner{width:100%;max-width:600px;max-height:100%;overflow:auto;-webkit-overflow-scrolling:touch;-ms-scroll-chaining:none;overscroll-behavior:contain;transform:translateY(20px);transition:transform .3s ease}.modal.visible .modal__inner{transform:none}.context{position:absolute;display:grid;top:0;left:0;padding:.5rem 0;min-width:210px;background:rgba(69,74,74,.98);border-radius:10px;transform:translate(var(--x),var(--y));z-index:3;opacity:0;pointer-events:none}@supports ((-webkit-backdrop-filter:blur(10px)) or (backdrop-filter:blur(10px))){.context{background:rgba(69,74,74,.86);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}}@media (max-width:800px){.context{font-size:.9em}}.context--floating{position:fixed;min-width:210px;border-radius:12px}.context.visible{opacity:1;pointer-events:all}.context__button{padding:.3333333333rem 1.2rem;color:currentColor;text-align:left;transition:color .3s ease,background .2s ease;line-height:1.4;outline:none}.context__button.active{color:#fff}.context__button:focus,.context__button:hover{background:hsla(0,0%,100%,.05)}.context--floating .context__button{margin:0 .5rem;padding:.5rem .625rem;color:#fff;border-radius:8px}.context--floating .context__button.active{color:#73fac8}.context__head{display:grid;grid-auto-flow:column;gap:1rem;align-items:center;justify-content:space-between}.context__label{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.context__description{padding-right:1rem;font-size:.86em;color:hsla(0,0%,100%,.5)}.context__separator{height:1px;background:hsla(0,0%,100%,.05);margin:.5rem 0}.filter{position:fixed;display:grid;justify-content:center;width:100%;bottom:4vh;z-index:2;pointer-events:none}@media (max-width:800px){.filter{font-size:.9em}}.filter__bar{display:grid;grid-auto-flow:column;gap:1rem;padding:0 1.5rem;border-radius:100px;background:rgba(69,74,74,.98);pointer-events:all}@supports ((-webkit-backdrop-filter:blur(10px)) or (backdrop-filter:blur(10px))){.filter__bar{background:rgba(69,74,74,.86);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}}.filter__button{display:flex;align-items:center;padding:.6666666667rem 0;white-space:nowrap;color:#fff;outline:none}.filter__button:focus{color:#73fac8}.filter__arrow{margin-left:.5rem;width:12px;height:12px;fill:hsla(0,0%,100%,.5)}.keyHint{display:flex;justify-content:center;width:1.5em;height:1.5em;font-size:.8em;background:hsla(0,0%,100%,.05);border-radius:3px;border:1px solid #6e7373;color:hsla(0,0%,100%,.5)}.keyHint,.status{align-items:center}.status{display:grid;gap:.5rem;grid-auto-flow:column;justify-content:start}.facts{--columns:3;grid-column:1/-1;display:grid;grid-template-columns:repeat(var(--columns),minmax(0,1fr));gap:1px;border-radius:10px;width:100%;overflow:hidden}@media (max-width:900px){.facts{--columns:2}}@media (max-width:560px){.facts{--columns:1}}.facts__card{display:grid;align-content:space-between;gap:1rem;background:#333838;padding:1.6rem 1.5rem 1.5rem;width:100%}.align-left{text-align:left}.align-right{text-align:right}.align-center{text-align:center}.spacer{height:calc(1rem*var(--size))}.color-primary{color:#73fac8}.color-white{color:#fff}.color-light{color:hsla(0,0%,100%,.5)}.color-black{color:#282d2d}.color-destructive{color:#ff3c3c} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 00000000..18b7e564 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,46 @@ +!function(){"use strict";"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self&&self;function e(e){if(e.__esModule)return e;var t=Object.defineProperty({},"__esModule",{value:!0});return Object.keys(e).forEach((function(n){var r=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,r.get?r:{enumerable:!0,get:function(){return e[n]}})})),t}function t(e){var t={exports:{}};return e(t,t.exports),t.exports +/* + object-assign + (c) Sindre Sorhus + @license MIT + */}var n=Object.getOwnPropertySymbols,r=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable;function o(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}var i=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var i,l,u=o(e),s=1;s=b},o=function(){},t.unstable_forceFrameRate=function(e){0>e||125>>1,a=e[r];if(!(void 0!==a&&0T(i,n))void 0!==u&&0>T(u,i)?(e[r]=u,e[l]=n,r=l):(e[r]=i,e[o]=n,r=o);else{if(!(void 0!==u&&0>T(u,n)))break e;e[r]=u,e[l]=n,r=l}}}return t}return null}function T(e,t){var n=e.sortIndex-t.sortIndex;return 0!==n?n:e.id-t.id}var x=[],N=[],C=1,O=null,P=3,R=!1,M=!1,D=!1;function L(e){for(var t=_(N);null!==t;){if(null===t.callback)S(N);else{if(!(t.startTime<=e))break;S(N),t.sortIndex=t.expirationTime,k(x,t)}t=_(N)}}function I(e){if(D=!1,L(e),!M)if(null!==_(x))M=!0,n(A);else{var t=_(N);null!==t&&r(I,t.startTime-e)}}function A(e,n){M=!1,D&&(D=!1,a()),R=!0;var o=P;try{for(L(n),O=_(x);null!==O&&(!(O.expirationTime>n)||e&&!t.unstable_shouldYield());){var i=O.callback;if("function"==typeof i){O.callback=null,P=O.priorityLevel;var l=i(O.expirationTime<=n);n=t.unstable_now(),"function"==typeof l?O.callback=l:O===_(x)&&S(x),L(n)}else S(x);O=_(x)}if(null!==O)var u=!0;else{var s=_(N);null!==s&&r(I,s.startTime-n),u=!1}return u}finally{O=null,P=o,R=!1}}var z=o;t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(e){e.callback=null},t.unstable_continueExecution=function(){M||R||(M=!0,n(A))},t.unstable_getCurrentPriorityLevel=function(){return P},t.unstable_getFirstCallbackNode=function(){return _(x)},t.unstable_next=function(e){switch(P){case 1:case 2:case 3:var t=3;break;default:t=P}var n=P;P=t;try{return e()}finally{P=n}},t.unstable_pauseExecution=function(){},t.unstable_requestPaint=z,t.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var n=P;P=e;try{return t()}finally{P=n}},t.unstable_scheduleCallback=function(e,o,i){var l=t.unstable_now();switch("object"==typeof i&&null!==i?i="number"==typeof(i=i.delay)&&0l?(e.sortIndex=i,k(N,e),null===_(x)&&e===_(N)&&(D?a():D=!0,r(I,i-l))):(e.sortIndex=u,k(x,e),M||R||(M=!0,n(A))),e},t.unstable_wrapCallback=function(e){var t=P;return function(){var n=P;P=t;try{return e.apply(this,arguments)}finally{P=n}}}})),c=(t((function(e,t){})),t((function(e){e.exports=s}))); +/** @license React v17.0.1 + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v17.0.1 + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +function d(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n