diff --git a/client/components/settings/adminReports.jade b/client/components/settings/adminReports.jade index ae32a1aa6..7a1b8cb96 100644 --- a/client/components/settings/adminReports.jade +++ b/client/components/settings/adminReports.jade @@ -1,5 +1,5 @@ template(name="adminReports") - .setting-content + .setting-content.admin-reports-content unless currentUser.isAdmin | {{_ 'error-notAuthorized'}} else @@ -26,6 +26,16 @@ template(name="adminReports") i.fa.fa-magic | {{_ 'rulesReportTitle'}} + li + a.js-report-boards(data-id="report-boards") + i.fa.fa-magic + | {{_ 'boardsReportTitle'}} + + li + a.js-report-cards(data-id="report-cards") + i.fa.fa-magic + | {{_ 'cardsReportTitle'}} + .main-body if loading.get +spinner @@ -37,6 +47,10 @@ template(name="adminReports") +orphanedFilesReport else if showRulesReport.get +rulesReport + else if showBoardsReport.get + +boardsReport + else if showCardsReport.get + +cardsReport template(name="brokenCardsReport") @@ -57,7 +71,7 @@ template(name="rulesReport") th actionType th activityType - each rule in rows + each rule in results tr td {{ rule.title }} td {{ rule.boardTitle }} @@ -78,7 +92,7 @@ template(name="filesReport") th MD5 Sum th ID - each att in attachmentFiles + each att in results tr td {{ att.filename }} td.right {{fileSize att.length }} @@ -100,7 +114,7 @@ template(name="orphanedFilesReport") th MD5 Sum th ID - each att in attachmentFiles + each att in results tr td {{ att.filename }} td.right {{fileSize att.length }} @@ -109,3 +123,50 @@ template(name="orphanedFilesReport") td {{ att._id.toHexString }} else div {{_ 'no-results' }} + +template(name="cardsReport") + h1 {{_ 'cardsReportTitle'}} + if resultsCount + table.table + tr + th Card Title + th Board + th Swimlane + th List + th Members + th Assignees + + each card in results + tr + td {{abbreviate card.title }} + td {{abbreviate card.board.title }} + td {{abbreviate card.swimlane.title }} + td {{abbreviate card.list.title }} + td {{userNames card.members }} + td {{userNames card.assignees }} + else + div {{_ 'no-results' }} + +template(name="boardsReport") + h1 {{_ 'boardsReportTitle'}} + if resultsCount + table.table + tr + th Title + th Id + th Permission + th Archived? + th Members + th Organizations + th Teams + + each board in results + tr + td {{abbreviate board.title }} + td {{abbreviate board._id }} + td {{ board.permission }} + td + = yesOrNo(board.archived) + td {{userNames board.members }} + else + div {{_ 'no-results' }} diff --git a/client/components/settings/adminReports.js b/client/components/settings/adminReports.js index e8ba75fc0..6dcbb0fc4 100644 --- a/client/components/settings/adminReports.js +++ b/client/components/settings/adminReports.js @@ -1,6 +1,8 @@ import { AttachmentStorage } from '/models/attachments'; import { CardSearchPagedComponent } from '/client/lib/cardSearch'; import SessionData from '/models/usersessiondata'; +import { QueryParams } from '/config/query-classes'; +import { OPERATOR_LIMIT } from '/config/search-const'; BlazeComponent.extendComponent({ subscription: null, @@ -8,10 +10,14 @@ BlazeComponent.extendComponent({ showBrokenCardsReport: new ReactiveVar(false), showOrphanedFilesReport: new ReactiveVar(false), showRulesReport: new ReactiveVar(false), + showCardsReport: new ReactiveVar(false), + showBoardsReport: new ReactiveVar(false), + sessionId: null, onCreated() { this.error = new ReactiveVar(''); this.loading = new ReactiveVar(false); + this.sessionId = SessionData.getSessionId(); }, events() { @@ -21,6 +27,8 @@ BlazeComponent.extendComponent({ 'click a.js-report-files': this.switchMenu, 'click a.js-report-orphaned-files': this.switchMenu, 'click a.js-report-rules': this.switchMenu, + 'click a.js-report-cards': this.switchMenu, + 'click a.js-report-boards': this.switchMenu, }, ]; }, @@ -32,6 +40,9 @@ BlazeComponent.extendComponent({ this.showFilesReport.set(false); this.showBrokenCardsReport.set(false); this.showOrphanedFilesReport.set(false); + this.showRulesReport.set(false) + this.showBoardsReport.set(false); + this.showCardsReport.set(false); if (this.subscription) { this.subscription.stop(); } @@ -64,68 +75,79 @@ BlazeComponent.extendComponent({ this.showRulesReport.set(true); this.loading.set(false); }); + } else if ('report-boards' === targetID) { + this.subscription = Meteor.subscribe('boardsReport', () => { + this.showBoardsReport.set(true); + this.loading.set(false); + }); + } else if ('report-cards' === targetID) { + const qp = new QueryParams(); + qp.addPredicate(OPERATOR_LIMIT, 300); + this.subscription = Meteor.subscribe( + 'globalSearch', + this.sessionId, + qp.getParams(), + qp.text, + () => { + this.showCardsReport.set(true); + this.loading.set(false); + }, + ); } } }, }).register('adminReports'); -Template.filesReport.helpers({ - attachmentFiles() { +class AdminReport extends BlazeComponent { + collection; + + results() { // eslint-disable-next-line no-console // console.log('attachments:', AttachmentStorage.find()); // console.log('attachments.count:', AttachmentStorage.find().count()); - return AttachmentStorage.find(); - }, - - rulesReport() { - const rules = []; - - Rules.find().forEach(rule => { - rules.push({ - _id: rule._id, - title: rule.title, - boardId: rule.boardId, - boardTitle: rule.board().title, - action: rule.action().fetch(), - trigger: rule.trigger().fetch(), - }); - }); + return this.collection.find(); + } - return rules; - }, + yesOrNo(value) { + if (value) { + return TAPi18n.__('yes'); + } else { + return TAPi18n.__('no'); + } + } resultsCount() { - return AttachmentStorage.find().count(); - }, + return this.collection.find().count(); + } fileSize(size) { return Math.round(size / 1024); - }, + } usageCount(key) { return Attachments.find({ 'copies.attachments.key': key }).count(); - }, -}); + } -Template.orphanedFilesReport.helpers({ - attachmentFiles() { - // eslint-disable-next-line no-console - // console.log('attachments:', AttachmentStorage.find()); - // console.log('attachments.count:', AttachmentStorage.find().count()); - return AttachmentStorage.find(); - }, + abbreviate(text) { + if (text.length > 30) { + return `${text.substr(0, 29)}...`; + } + return text; + } +} - resultsCount() { - return AttachmentStorage.find().count(); - }, +(class extends AdminReport { + collection = AttachmentStorage; +}.register('filesReport')); - fileSize(size) { - return Math.round(size / 1024); - }, -}); +(class extends AdminReport { + collection = AttachmentStorage; +}.register('orphanedFilesReport')); + +(class extends AdminReport { + collection = Rules; -Template.rulesReport.helpers({ - rows() { + results() { const rules = []; Rules.find().forEach(rule => { @@ -139,14 +161,43 @@ Template.rulesReport.helpers({ }); }); + // eslint-disable-next-line no-console console.log('rows:', rules); return rules; - }, + } +}.register('rulesReport')); + +(class extends AdminReport { + collection = Boards; + + userNames(members) { + let text = ''; + members.forEach(member => { + const user = Users.findOne(member.userId); + text += text ? ', ' : ''; + if (user) { + text += user.username; + } else { + text += member.userId + } + }); + return text; + } +}.register('boardsReport')); - resultsCount() { - return Rules.find().count(); - }, -}); +(class extends AdminReport { + collection = Cards; + + userNames(userIds) { + let text = ''; + userIds.forEach(userId => { + const user = Users.findOne(userId); + text += text ? ', ' : ''; + text += user.username; + }); + return text; + } +}.register('cardsReport')); class BrokenCardsComponent extends CardSearchPagedComponent { onCreated() { diff --git a/client/components/settings/adminReports.styl b/client/components/settings/adminReports.styl new file mode 100644 index 000000000..3a5234842 --- /dev/null +++ b/client/components/settings/adminReports.styl @@ -0,0 +1,3 @@ +.admin-reports-content + height: auto !important + diff --git a/config/const.js b/config/const.js index a275ffa6b..8bbc7d13e 100644 --- a/config/const.js +++ b/config/const.js @@ -49,6 +49,14 @@ export const TYPE_LINKED_BOARD = 'cardType-linkedBoard'; export const TYPE_LINKED_CARD = 'cardType-linkedCard'; export const TYPE_TEMPLATE_BOARD = 'template-board'; export const TYPE_TEMPLATE_CONTAINER = 'template-container'; +export const TYPE_TEMPLATE_CARD = 'template-card'; +export const TYPE_TEMPLATE_LIST = 'template-list'; +export const CARD_TYPES = [ + TYPE_CARD, + TYPE_LINKED_CARD, + TYPE_LINKED_BOARD, + TYPE_TEMPLATE_CARD +]; export const ALLOWED_WAIT_SPINNERS = [ 'Bounce', 'Cube', diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 5bbc902de..02d70f42d 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -1065,6 +1065,8 @@ "orphanedFilesReportTitle": "Orphaned Files Report", "reports": "Reports", "rulesReportTitle": "Rules Report", + "boardsReportTitle": "Boards Report", + "cardsReportTitle": "Cards Report", "copy-swimlane": "Copy Swimlane", "copySwimlanePopup-title": "Copy Swimlane", "display-card-creator": "Display Card Creator", diff --git a/models/boards.js b/models/boards.js index 1195b5891..6244e5aca 100644 --- a/models/boards.js +++ b/models/boards.js @@ -5,6 +5,7 @@ import { TYPE_TEMPLATE_BOARD, TYPE_TEMPLATE_CONTAINER, } from '/config/const'; +import Users from "./users"; const escapeForRegex = require('escape-string-regexp'); Boards = new Mongo.Collection('boards'); @@ -1479,7 +1480,17 @@ Boards.userSearch = ( return Boards.find(selector, projection); }; -Boards.userBoards = (userId, archived = false, selector = {}) => { +Boards.userBoards = ( + userId, + archived = false, + selector = {}, + projection = {}, +) => { + const user = Users.findOne(userId); + if (!user) { + return []; + } + if (typeof archived === 'boolean') { selector.archived = archived; } @@ -1487,15 +1498,20 @@ Boards.userBoards = (userId, archived = false, selector = {}) => { selector.type = 'board'; } - selector.$or = [{ permission: 'public' }]; - if (userId) { - selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } }); - } - return Boards.find(selector); + selector.$or = [ + { permission: 'public' }, + { members: { $elemMatch: { userId, isActive: true } } }, + { 'orgs.orgId': { $in: user.orgIds() } }, + { 'teams.teamId': { $in : user.teamIds() } }, + ]; + + return Boards.find(selector, projection); }; Boards.userBoardIds = (userId, archived = false, selector = {}) => { - return Boards.userBoards(userId, archived, selector).map(board => { + return Boards.userBoards(userId, archived, selector, { + fields: { _id: 1 }, + }).map(board => { return board._id; }); }; diff --git a/models/users.js b/models/users.js index 70a5b141b..05354e045 100644 --- a/models/users.js +++ b/models/users.js @@ -519,6 +519,18 @@ Users.helpers({ } return ''; }, + teamIds() { + if (this.teams) { + return this.teams.map(team => { return team.teamId }); + } + return []; + }, + orgIds() { + if (this.orgs) { + return this.orgs.map(org => { return org.orgId }); + } + return []; + }, orgsUserBelongs() { if (this.orgs) { return this.orgs.map(function(org){return org.orgDisplayName}).sort().join(','); @@ -544,32 +556,16 @@ Users.helpers({ return ''; }, boards() { - return Boards.find( - { - 'members.userId': this._id, - }, - { - sort: { - sort: 1 /* boards default sorting */, - }, - }, - ); + return Boards.userBoards(this._id, null, {}, { sort: { sort: 1 } }) }, starredBoards() { const { starredBoards = [] } = this.profile || {}; - return Boards.find( - { - archived: false, - _id: { - $in: starredBoards, - }, - }, - { - sort: { - sort: 1 /* boards default sorting */, - }, - }, + return Boards.userBoards( + this._id, + false, + { _id: { $in: starredBoards } }, + { sort: { sort: 1 } } ); }, @@ -580,18 +576,11 @@ Users.helpers({ invitedBoards() { const { invitedBoards = [] } = this.profile || {}; - return Boards.find( - { - archived: false, - _id: { - $in: invitedBoards, - }, - }, - { - sort: { - sort: 1 /* boards default sorting */, - }, - }, + return Boards.userBoards( + this._id, + false, + { _id: { $in: invitedBoards } }, + { sort: { sort: 1 } } ); }, diff --git a/releases/release-bundle.sh b/releases/release-bundle.sh index 309c041a0..65518c2b1 100755 --- a/releases/release-bundle.sh +++ b/releases/release-bundle.sh @@ -6,11 +6,11 @@ cd ~/repos/wekan/.build zip -r wekan-$1.zip bundle { - scp ~/repos/wekan/releases/maintainer-make-bundle-a.sh a:/home/wekan/repos/maintainer-make-bundle.sh + scp ~/repos/wekan/releases/maintainer-make-bundle-a.sh a:/home/wekan/maintainer-make-bundle.sh scp ~/repos/wekan/releases/maintainer-make-bundle-s.sh s:/home/linux1/maintainer-make-bundle.sh scp ~/repos/wekan/releases/maintainer-make-bundle-o.sh o:/home/ubuntu/maintainer-make-bundle.sh scp wekan-$1.zip x2:/var/snap/wekan/common/releases.wekan.team/ - scp wekan-$1.zip a:/home/wekan/repos/ + scp wekan-$1.zip a:/home/wekan/ scp wekan-$1.zip s:/home/linux1/ scp wekan-$1.zip o:/home/ubuntu/ } | parallel -k diff --git a/releases/up-a.sh b/releases/up-a.sh index 7eebc9270..019ab34ca 100755 --- a/releases/up-a.sh +++ b/releases/up-a.sh @@ -11,7 +11,7 @@ if [ $# -ne 1 ] fi # 2) Download release from arm64 build server -scp a:/home/wekan/repos/wekan-$1-arm64.zip . +scp a:/home/wekan/wekan-$1-arm64.zip . # 3) Upload arm64 release to download server scp wekan-$1-arm64.zip x2:/var/snap/wekan/common/releases.wekan.team/raspi3/ diff --git a/server/publications/boards.js b/server/publications/boards.js index 23306e087..ef47e52bb 100644 --- a/server/publications/boards.js +++ b/server/publications/boards.js @@ -2,40 +2,47 @@ // non-archived boards: // 1. that the user is a member of // 2. the user has starred +import Users from "../../models/users"; +import Org from "../../models/org"; +import Team from "../../models/team"; + Meteor.publish('boards', function() { const userId = this.userId; // Ensure that the user is connected. If it is not, we need to return an empty // array to tell the client to remove the previously published docs. - if (!Match.test(userId, String) || !userId) return []; + if (!Match.test(userId, String) || !userId) { + return []; + } // Defensive programming to verify that starredBoards has the expected // format -- since the field is in the `profile` a user can modify it. - const { starredBoards = [] } = (Users.findOne(userId) || {}).profile || {}; - check(starredBoards, [String]); + // const { starredBoards = [] } = (Users.findOne(userId) || {}).profile || {}; + // check(starredBoards, [String]); - let currUser = Users.findOne(userId); - let orgIdsUserBelongs = currUser!== 'undefined' && currUser.teams !== 'undefined' ? currUser.orgIdsUserBelongs() : ''; - let teamIdsUserBelongs = currUser!== 'undefined' && currUser.teams !== 'undefined' ? currUser.teamIdsUserBelongs() : ''; - let orgsIds = []; - let teamsIds = []; - if(orgIdsUserBelongs && orgIdsUserBelongs != ''){ - orgsIds = orgIdsUserBelongs.split(','); - } - if(teamIdsUserBelongs && teamIdsUserBelongs != ''){ - teamsIds = teamIdsUserBelongs.split(','); - } + // let currUser = Users.findOne(userId); + // let orgIdsUserBelongs = currUser!== 'undefined' && currUser.teams !== 'undefined' ? currUser.orgIdsUserBelongs() : ''; + // let teamIdsUserBelongs = currUser!== 'undefined' && currUser.teams !== 'undefined' ? currUser.teamIdsUserBelongs() : ''; + // let orgsIds = []; + // let teamsIds = []; + // if(orgIdsUserBelongs && orgIdsUserBelongs != ''){ + // orgsIds = orgIdsUserBelongs.split(','); + // } + // if(teamIdsUserBelongs && teamIdsUserBelongs != ''){ + // teamsIds = teamIdsUserBelongs.split(','); + // } return Boards.find( { archived: false, - $or: [ - { - // _id: { $in: starredBoards }, // Commented out, to get a list of all public boards - permission: 'public', - }, - { members: { $elemMatch: { userId, isActive: true } } }, - {'orgs.orgId': {$in : orgsIds}}, - {'teams.teamId': {$in : teamsIds}}, - ], + _id: { $in: Boards.userBoardIds(userId, false) }, + // $or: [ + // { + // // _id: { $in: starredBoards }, // Commented out, to get a list of all public boards + // permission: 'public', + // }, + // { members: { $elemMatch: { userId, isActive: true } } }, + // {'orgs.orgId': {$in : orgsIds}}, + // {'teams.teamId': {$in : teamsIds}}, + // ], }, { fields: { @@ -58,19 +65,79 @@ Meteor.publish('boards', function() { ); }); +Meteor.publish('boardsReport', function() { + const userId = this.userId; + // Ensure that the user is connected. If it is not, we need to return an empty + // array to tell the client to remove the previously published docs. + if (!Match.test(userId, String) || !userId) return []; + + const boards = Boards.find( + { + _id: { $in: Boards.userBoardIds(userId, null) }, + }, + { + fields: { + _id: 1, + boardId: 1, + archived: 1, + slug: 1, + title: 1, + description: 1, + color: 1, + members: 1, + orgs: 1, + teams: 1, + permission: 1, + type: 1, + sort: 1, + }, + sort: { sort: 1 /* boards default sorting */ }, + }, + ); + + const userIds = []; + const orgIds = []; + const teamIds = []; + boards.forEach(board => { + if (board.members) { + board.members.forEach(member => { + userIds.push(member.userId); + }); + } + if (board.orgs) { + board.orgs.forEach(org => { + orgIds.push(org.orgId); + }); + } + if (board.teams) { + board.teams.forEach(team => { + teamIds.push(team.teamId); + }); + } + }) + + return [ + boards, + Users.find({ _id: { $in: userIds } }, { fields: Users.safeFields }), + Team.find({ _id: { $in: teamIds } }), + Org.find({ _id: { $in: orgIds } }), + ] +}); + Meteor.publish('archivedBoards', function() { const userId = this.userId; if (!Match.test(userId, String)) return []; return Boards.find( { - archived: true, - members: { - $elemMatch: { - userId, - isAdmin: true, - }, - }, + _id: { $in: Boards.userBoardIds(userId, true)}, + // archived: true, + // members: { + // $elemMatch: { + // userId, + // isAdmin: true, + // }, + // }, }, { fields: { diff --git a/server/publications/cards.js b/server/publications/cards.js index ca9f7a10c..7f7348a08 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -48,6 +48,7 @@ import { PREDICATE_SYSTEM, } from '/config/search-const'; import { QueryErrors, QueryParams, Query } from '/config/query-classes'; +import { CARD_TYPES } from '../../config/const'; const escapeForRegex = require('escape-string-regexp'); @@ -587,6 +588,7 @@ Meteor.publish('brokenCards', function(sessionId) { { boardId: { $in: [null, ''] } }, { swimlaneId: { $in: [null, ''] } }, { listId: { $in: [null, ''] } }, + { type: { $nin: CARD_TYPES } }, ]; // console.log('brokenCards selector:', query.selector);