@openneuro/app 4.27.0 → 4.28.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openneuro/app",
3
- "version": "4.27.0",
3
+ "version": "4.28.0-alpha.1",
4
4
  "description": "React JS web frontend for the OpenNeuro platform.",
5
5
  "license": "MIT",
6
6
  "main": "public/client.js",
@@ -19,8 +19,8 @@
19
19
  "@emotion/react": "11.11.1",
20
20
  "@emotion/styled": "11.11.0",
21
21
  "@niivue/niivue": "0.34.0",
22
- "@openneuro/client": "^4.27.0",
23
- "@openneuro/components": "^4.27.0",
22
+ "@openneuro/client": "^4.28.0-alpha.1",
23
+ "@openneuro/components": "^4.28.0-alpha.1",
24
24
  "@sentry/react": "^8.25.0",
25
25
  "@tanstack/react-table": "^8.9.3",
26
26
  "bids-validator": "1.14.8",
@@ -75,5 +75,5 @@
75
75
  "publishConfig": {
76
76
  "access": "public"
77
77
  },
78
- "gitHead": "ee90f11f7696e905f477b04b713a59caff374c30"
78
+ "gitHead": "ed13f51c2529a7827bfa8ba8de98f1dc7b68aba7"
79
79
  }
@@ -32,7 +32,7 @@ const DeleteDataset = ({ datasetId, metadata }) => {
32
32
  },
33
33
  })
34
34
  window.location.replace(
35
- `${window.location.origin}/dashboard/datasets`,
35
+ `${window.location.origin}/search`,
36
36
  )
37
37
  })}
38
38
  >
@@ -32,7 +32,7 @@ const DeleteDataset = ({ datasetId, metadata }) => {
32
32
  },
33
33
  })
34
34
  window.location.replace(
35
- `${window.location.origin}/dashboard/datasets`,
35
+ `${window.location.origin}/search`,
36
36
  )
37
37
  })}
38
38
  >
@@ -0,0 +1,89 @@
1
+ import React from "react"
2
+ import { fireEvent, render, screen } from "@testing-library/react"
3
+ import { MockedProvider } from "@apollo/client/testing"
4
+ import { GET_USERS, UsersQuery } from "../users"
5
+ import { vi } from "vitest"
6
+
7
+ // Mock admin login
8
+ vi.mock("../../../authentication/profile", (_importOriginal) => {
9
+ return {
10
+ getProfile: () => {
11
+ return {
12
+ sub: "1234",
13
+ admin: true,
14
+ }
15
+ },
16
+ }
17
+ })
18
+
19
+ const users = [
20
+ {
21
+ __typename: "User",
22
+ id: "77106418-0daf-4800-a17c-71657ede4b21",
23
+ name: "Test Users",
24
+ admin: false,
25
+ blocked: false,
26
+ email: "test@example.com",
27
+ provider: "test",
28
+ lastSeen: "2018-09-24T19:26:07.704Z",
29
+ created: "2016-09-24T19:26:07.704Z",
30
+ },
31
+ ]
32
+
33
+ describe("Users", () => {
34
+ it("renders users", async () => {
35
+ const mocks = [
36
+ {
37
+ delay: 30,
38
+ request: { query: GET_USERS },
39
+ result: {
40
+ data: { users },
41
+ },
42
+ },
43
+ ]
44
+
45
+ render(
46
+ <MockedProvider mocks={mocks}>
47
+ <UsersQuery />
48
+ </MockedProvider>,
49
+ )
50
+
51
+ expect(await screen.findByText("Current Users")).toBeInTheDocument()
52
+ expect(await screen.findByText(users[0].name)).toBeInTheDocument()
53
+ })
54
+ it("handles filtering users with no email", async () => {
55
+ const emailLessUsers = [...users, {
56
+ __typename: "User",
57
+ id: "db3e7a0b-950b-4951-9059-c003ca3c1669",
58
+ name: "New User",
59
+ admin: false,
60
+ email: null,
61
+ blocked: false,
62
+ provider: "orcid",
63
+ lastSeen: "2019-09-24T19:26:07.704Z",
64
+ created: "2013-09-24T19:26:07.704Z",
65
+ }]
66
+ const mocks = [
67
+ {
68
+ delay: 30,
69
+ request: { query: GET_USERS },
70
+ result: {
71
+ data: { users: emailLessUsers },
72
+ },
73
+ },
74
+ ]
75
+
76
+ render(
77
+ <MockedProvider mocks={mocks}>
78
+ <UsersQuery />
79
+ </MockedProvider>,
80
+ )
81
+
82
+ const input = await screen.findByPlaceholderText("Search Name or Email")
83
+ fireEvent.change(input, { target: { value: "test" } })
84
+ fireEvent.keyDown(input, { key: "a" })
85
+
86
+ expect(await screen.findByText("Current Users")).toBeInTheDocument()
87
+ expect(await screen.findByText(users[0].name)).toBeInTheDocument()
88
+ })
89
+ })
@@ -2,7 +2,7 @@
2
2
 
3
3
  import React from "react"
4
4
  import { Navigate, NavLink, Route, Routes } from "react-router-dom"
5
- import Users from "./users.jsx"
5
+ import Users from "./users"
6
6
  import FlaggedFiles from "./flagged-files.jsx"
7
7
  import AdminUser from "../../authentication/admin-user.jsx"
8
8
 
@@ -0,0 +1,185 @@
1
+ import React, { useState } from "react"
2
+ import { Query } from "@apollo/client/react/components"
3
+ import { gql } from "@apollo/client"
4
+ import parseISO from "date-fns/parseISO"
5
+ import formatDistanceToNow from "date-fns/formatDistanceToNow"
6
+ import { Input } from "@openneuro/components/input"
7
+ import { Loading } from "@openneuro/components/loading"
8
+ import { formatDate } from "../../utils/date.js"
9
+ import Helmet from "react-helmet"
10
+ import { pageTitle } from "../../resources/strings.js"
11
+ import { UserTools } from "./user-tools.js"
12
+ import { USER_FRAGMENT } from "./user-fragment.js"
13
+
14
+ export const GET_USERS = gql`
15
+ query {
16
+ users {
17
+ ...userFields
18
+ }
19
+ }
20
+ ${USER_FRAGMENT}
21
+ `
22
+
23
+ // TODO - Use the GraphQL type
24
+ export interface User {
25
+ id: string
26
+ name: string
27
+ admin: boolean
28
+ blocked: boolean
29
+ email?: string
30
+ provider: string
31
+ lastSeen?: string
32
+ created: string
33
+ }
34
+
35
+ interface UsersQueryResultProps {
36
+ loading: boolean
37
+ data: { users: User[] }
38
+ refetch: () => void
39
+ }
40
+
41
+ export const UsersQueryResult = (
42
+ { loading, data, refetch }: UsersQueryResultProps,
43
+ ) => {
44
+ if (loading) {
45
+ return <Loading />
46
+ } else {
47
+ return (
48
+ <Users loading={loading} users={data.users || []} refetch={refetch} />
49
+ )
50
+ }
51
+ }
52
+
53
+ export const UsersQuery = () => (
54
+ <Query query={GET_USERS}>{UsersQueryResult}</Query>
55
+ )
56
+
57
+ const userSummary = (user) => {
58
+ const lastLogin = user.lastlogin ? user.lastlogin : user.created
59
+ const created = user.created
60
+ return (
61
+ <>
62
+ <div className="summary-data">
63
+ <b>Signed Up:</b>{" "}
64
+ <div>
65
+ {formatDate(created)} - {formatDistanceToNow(parseISO(created))} ago
66
+ </div>
67
+ </div>
68
+ <div className="summary-data">
69
+ <b>Last Signed In:</b>{" "}
70
+ <div>
71
+ {formatDate(lastLogin)} - {formatDistanceToNow(parseISO(lastLogin))}
72
+ {" "}
73
+ ago
74
+ </div>
75
+ </div>
76
+ </>
77
+ )
78
+ }
79
+
80
+ const noResults = (loading) => {
81
+ return loading ? <Loading /> : <h4>No Results Found</h4>
82
+ }
83
+
84
+ const Users = ({ users, refetch, loading }) => {
85
+ const [stringFilter, setStringFilter] = useState(null)
86
+ const [adminFilter, setAdminFilter] = useState(false)
87
+ const [blacklistFilter, setBlacklistFilter] = useState(false)
88
+
89
+ const filteredUsers = users
90
+ .filter((user) => !adminFilter || user.admin)
91
+ .filter(
92
+ (user) =>
93
+ !stringFilter ||
94
+ user.email?.toLowerCase().includes(stringFilter.toLowerCase()) ||
95
+ user.name?.toLowerCase().includes(stringFilter.toLowerCase()),
96
+ )
97
+ .map((user, index) => {
98
+ const adminBadge = user.admin ? "Admin" : null
99
+ const userEmail = user.hasOwnProperty("email") ? user.email : user.id
100
+ return (
101
+ <div className="fade-in user-panel panel panel-default" key={index}>
102
+ <div className="user-col uc-name">
103
+ <div>
104
+ {user.name}{" "}
105
+ {adminBadge && <span className="badge">{adminBadge}</span>}
106
+ <UserTools user={user} refetch={refetch} />
107
+ </div>
108
+ </div>
109
+ <div className="user-col user-panel-inner">
110
+ <div className=" user-col uc-email">
111
+ {userEmail}
112
+ <div className=" uc-provider">
113
+ <b>Provider:</b> {user.provider}
114
+ </div>
115
+ </div>
116
+
117
+ <div className=" user-col uc-summary">{userSummary(user)}</div>
118
+ </div>
119
+ </div>
120
+ )
121
+ })
122
+
123
+ return (
124
+ <>
125
+ <Helmet>
126
+ <title>Admin Dashboard - {pageTitle}</title>
127
+ </Helmet>
128
+ <div className="admin-users">
129
+ <div className="header-wrap ">
130
+ <h2>Current Users</h2>
131
+
132
+ <Input
133
+ name="Search Name Or Email"
134
+ type="text"
135
+ placeholder="Search Name or Email"
136
+ onKeyDown={(e) => setStringFilter(e.target.value)}
137
+ setValue={(_) => {}}
138
+ />
139
+ </div>
140
+
141
+ <div className="filters-sort-wrap ">
142
+ <span>
143
+ <div className="filters">
144
+ <label>Filter By:</label>
145
+ <button
146
+ className={adminFilter ? "active" : null}
147
+ onClick={() => setAdminFilter(!adminFilter)}
148
+ >
149
+ <span className="filter-admin">
150
+ <i
151
+ className={adminFilter
152
+ ? "fa fa-check-square-o"
153
+ : "fa fa-square-o"}
154
+ />{" "}
155
+ Admin
156
+ </span>
157
+ </button>
158
+ <button
159
+ className={blacklistFilter ? "active" : null}
160
+ onClick={() => setBlacklistFilter(!blacklistFilter)}
161
+ >
162
+ <span className="filter-admin">
163
+ <i
164
+ className={blacklistFilter
165
+ ? "fa fa-check-square-o"
166
+ : "fa fa-square-o"}
167
+ />{" "}
168
+ Blocked
169
+ </span>
170
+ </button>
171
+ </div>
172
+ </span>
173
+ </div>
174
+
175
+ <div>
176
+ <div className="users-panel-wrap">
177
+ {filteredUsers.length ? filteredUsers : noResults(loading)}
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </>
182
+ )
183
+ }
184
+
185
+ export default UsersQuery
@@ -1,202 +0,0 @@
1
- // dependencies -------------------------------------------------------
2
-
3
- import React from "react"
4
- import PropTypes from "prop-types"
5
- import { Query } from "@apollo/client/react/components"
6
- import { gql } from "@apollo/client"
7
- import parseISO from "date-fns/parseISO"
8
- import formatDistanceToNow from "date-fns/formatDistanceToNow"
9
- import { Input } from "@openneuro/components/input"
10
- import { Loading } from "@openneuro/components/loading"
11
- import { formatDate } from "../../utils/date.js"
12
- import Helmet from "react-helmet"
13
- import { pageTitle } from "../../resources/strings.js"
14
- import { UserTools } from "./user-tools"
15
- import { USER_FRAGMENT } from "./user-fragment"
16
-
17
- export const GET_USERS = gql`
18
- query {
19
- users {
20
- ...userFields
21
- }
22
- }
23
- ${USER_FRAGMENT}
24
- `
25
-
26
- export const UsersQueryResult = ({ loading, data, refetch }) => {
27
- if (loading) {
28
- return <Loading />
29
- } else {
30
- return (
31
- <Users loading={loading} users={data.users || []} refetch={refetch} />
32
- )
33
- }
34
- }
35
-
36
- export const UsersQuery = () => (
37
- <Query query={GET_USERS}>{UsersQueryResult}</Query>
38
- )
39
-
40
- UsersQueryResult.propTypes = {
41
- loading: PropTypes.bool,
42
- data: PropTypes.object,
43
- refetch: PropTypes.func,
44
- }
45
-
46
- class Users extends React.Component {
47
- constructor(props) {
48
- super(props)
49
- this.state = {
50
- stringFilter: null,
51
- adminFilter: false,
52
- blacklistFilter: false,
53
- }
54
- }
55
-
56
- // life cycle events --------------------------------------------------
57
- render() {
58
- const users = this.props.users.map((user, index) => {
59
- const adminBadge = user.admin ? "Admin" : null
60
- const userEmail = user.hasOwnProperty("email") ? user.email : user.id
61
- if (this.state.adminFilter && !user.admin) {
62
- return null
63
- }
64
- if (this.state.stringFilter) {
65
- const stringFilter = this.state.stringFilter.toLowerCase()
66
- if (
67
- !(
68
- user.email.toLowerCase().includes(stringFilter) ||
69
- user.name.toLowerCase().includes(stringFilter)
70
- )
71
- ) {
72
- return null
73
- }
74
- }
75
- return (
76
- <div className="fade-in user-panel panel panel-default" key={index}>
77
- <div className="user-col uc-name">
78
- <div>
79
- {user.name}{" "}
80
- {adminBadge && <span className="badge">{adminBadge}</span>}
81
- <UserTools user={user} refetch={this.props.refetch} />
82
- </div>
83
- </div>
84
- <div className="user-col user-panel-inner">
85
- <div className=" user-col uc-email">
86
- {userEmail}
87
- <div className=" uc-provider">
88
- <b>Provider:</b> {user.provider}
89
- </div>
90
- </div>
91
-
92
- <div className=" user-col uc-summary">
93
- {this._userSummary(user)}
94
- </div>
95
- </div>
96
- </div>
97
- )
98
- })
99
- return (
100
- <>
101
- <Helmet>
102
- <title>Admin Dashboard - {pageTitle}</title>
103
- </Helmet>
104
- <div className="admin-users">
105
- <div className="header-wrap ">
106
- <h2>Current Users</h2>
107
-
108
- <Input
109
- name="Search Name Or Email"
110
- type="text"
111
- placeholder="Search Name or Email"
112
- onKeyDown={(e) => this.setState({ stringFilter: e.target.value })}
113
- />
114
- </div>
115
-
116
- <div className="filters-sort-wrap ">
117
- <span>
118
- <div className="filters">
119
- <label>Filter By:</label>
120
- <button
121
- className={this.state.adminFilter ? "active" : null}
122
- onClick={() =>
123
- this.setState({ adminFilter: !this.state.adminFilter })}
124
- >
125
- <span className="filter-admin">
126
- <i
127
- className={this.state.adminFilter
128
- ? "fa fa-check-square-o"
129
- : "fa fa-square-o"}
130
- />{" "}
131
- Admin
132
- </span>
133
- </button>
134
- <button
135
- className={this.state.blacklistFilter ? "active" : null}
136
- onClick={() =>
137
- this.setState({
138
- blacklistFilter: !this.state.blacklistFilter,
139
- })}
140
- >
141
- <span className="filter-admin">
142
- <i
143
- className={this.state.blacklistFilter
144
- ? "fa fa-check-square-o"
145
- : "fa fa-square-o"}
146
- />{" "}
147
- Blocked
148
- </span>
149
- </button>
150
- </div>
151
- </span>
152
- </div>
153
-
154
- <div>
155
- <div className="users-panel-wrap">
156
- {users.filter((u) => u !== null).length
157
- ? users
158
- : this._noResults()}
159
- </div>
160
- </div>
161
- </div>
162
- </>
163
- )
164
- }
165
-
166
- // custom methods -----------------------------------------------------
167
-
168
- _noResults() {
169
- return this.state.loading ? <Loading /> : <h4>No Results Found</h4>
170
- }
171
-
172
- _userSummary(user) {
173
- const lastLogin = user.lastlogin ? user.lastlogin : user.created
174
- const created = user.created
175
- return (
176
- <>
177
- <div className="summary-data">
178
- <b>Signed Up:</b>{" "}
179
- <div>
180
- {formatDate(created)} - {formatDistanceToNow(parseISO(created))} ago
181
- </div>
182
- </div>
183
- <div className="summary-data">
184
- <b>Last Signed In:</b>{" "}
185
- <div>
186
- {formatDate(lastLogin)} - {formatDistanceToNow(parseISO(lastLogin))}
187
- {" "}
188
- ago
189
- </div>
190
- </div>
191
- </>
192
- )
193
- }
194
- }
195
-
196
- Users.propTypes = {
197
- users: PropTypes.array,
198
- loading: PropTypes.bool,
199
- refetch: PropTypes.func,
200
- }
201
-
202
- export default UsersQuery