Skip to content

Commit d3a4491

Browse files
bors[bot]bidoubiwaLtdJorgedependabot[bot]
authored
Merge #77
77: Step 4: Hooks r=bidoubiwa a=bidoubiwa In this PR ### On launch of server: Create hooks by surcharging lifecycles functions of collections that are indexed in MeiliSearch ### On the plugin page - A reload server button when hooks must be updated - A hooks column that mentioned you should reload or if hooks are active - An update button if you want to update manually your data to MeiliSearch fixes: #79 -> automatic hooks fixes: #18 -> Hooks fixes: #16 -> Step 4 fixes: #58 -> If collection is in draft mode it is still added to MeiliSearch if the users ask for it fixes: #84 -> server reload brings back to plugin page Co-authored-by: Charlotte Vermandel <[email protected]> Co-authored-by: bors[bot] <26634292+bors[bot]@users.noreply.github.com> Co-authored-by: LtdJorge <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2 parents d4e2431 + dbe55bc commit d3a4491

File tree

30 files changed

+771
-193
lines changed

30 files changed

+771
-193
lines changed

.eslintrc.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ module.exports = {
1919
},
2020
ecmaVersion: 11
2121
},
22-
ignorePatterns: ['playground'],
22+
ignorePatterns: ['playground/plugins'],
2323
plugins: [
2424
'react'
2525
],
@@ -33,6 +33,8 @@ module.exports = {
3333
'react/jsx-indent-props': [2, 2],
3434
'cypress/no-unnecessary-waiting': 'off',
3535
'react/prop-types': 'off',
36-
'react/jsx-closing-bracket-location': [2, 'tag-aligned']
36+
'react/jsx-closing-bracket-location': [2, 'tag-aligned'],
37+
'no-unused-vars': ['error', { varsIgnorePattern: '^omit.*$' }],
38+
'array-callback-return': 'off'
3739
}
3840
}

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
uses: actions/setup-node@v1
2929
- name: Install dependencies
3030
run: yarn --dev && yarn --cwd ./playground
31-
- name: Cypress run
31+
- name: Test develop no-reload
3232
uses: cypress-io/github-action@v2
3333
with:
3434
build: yarn playground:build

admin/src/components/Collections.js

Lines changed: 111 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,21 @@
66
import React, { memo, useState, useEffect } from 'react'
77
import { request } from 'strapi-helper-plugin'
88
import pluginId from '../pluginId'
9-
import { Table } from '@buffetjs/core'
9+
import { Table, Button } from '@buffetjs/core'
1010
import { errorNotifications, successNotification } from '../utils/notifications'
1111
import { Wrapper } from '../components/Wrapper'
12+
import styled from 'styled-components'
13+
14+
export const UpdateButton = styled(Button)`
15+
display: flex;
16+
align-items: center;
17+
`
18+
19+
export const ReloadButton = styled(Button)`
20+
display: flex;
21+
align-items: center;
22+
color: delete;
23+
`
1224

1325
const headers = [
1426
{
@@ -18,53 +30,77 @@ const headers = [
1830
{
1931
name: 'Status',
2032
value: 'status'
33+
},
34+
{
35+
name: 'Hooks',
36+
value: 'hooked'
2137
}
2238
]
2339

2440
const Collections = ({ updateCredentials }) => {
2541
const [collectionsList, setCollectionsList] = useState([])
2642
const [updatedCollections, setUpdatedCollections] = useState(false)
43+
const [needReload, setNeedReload] = useState(false)
2744

28-
const updateStatus = async ({ indexUid, updateId }) => {
29-
const response = await request(`/${pluginId}/indexes/${indexUid}/update/${updateId}`, {
30-
method: 'GET'
31-
})
32-
const { error } = response
33-
if (error) errorNotifications(error)
34-
else successNotification({ message: `${indexUid} has all its documents indexed` })
35-
setUpdatedCollections(false)
45+
const updateStatus = async ({ collection, updateId }) => {
46+
if (updateId) {
47+
const response = await request(`/${pluginId}/indexes/${collection}/update/${updateId}`, {
48+
method: 'GET'
49+
})
50+
const { error } = response
51+
if (error) errorNotifications(error)
52+
else successNotification({ message: `${collection} has all its documents indexed` })
53+
setUpdatedCollections(false)
54+
}
3655
}
3756

38-
const addCollectionToMeiliSearch = async ({ name: indexUid }) => {
39-
const update = await request(`/${pluginId}/collections/`, {
40-
method: 'POST',
41-
body: {
42-
indexUid
43-
}
57+
const addCollection = async ({ name: collection }) => {
58+
const update = await request(`/${pluginId}/collections/${collection}/`, {
59+
method: 'POST'
4460
})
4561
if (update.error) {
4662
errorNotifications(update)
4763
} else {
48-
successNotification({ message: `${indexUid} is created!` })
64+
successNotification({ message: `${collection} is created!`, duration: 4000 })
4965
setCollectionsList(prev => prev.map(col => {
50-
if (col.name === indexUid) col.status = 'enqueued'
66+
if (col.name === collection) col.status = 'enqueued'
5167
return col
5268
}))
53-
updateStatus({ indexUid, updateId: update.updateId })
69+
updateStatus({ collection, updateId: update.updateId })
70+
}
71+
}
72+
73+
const updateCollections = async ({ collection }) => {
74+
try {
75+
const update = await request(`/${pluginId}/collections/${collection}/`, {
76+
method: 'PUT'
77+
})
78+
if (update.error) {
79+
errorNotifications(update)
80+
} else {
81+
successNotification({ message: `${collection} updated!` })
82+
setCollectionsList(prev => prev.map(col => {
83+
if (col.name === collection) col.status = 'enqueued'
84+
return col
85+
}))
86+
updateStatus({ collection, updateId: update.updateId })
87+
}
88+
} catch (e) {
89+
console.error(e)
5490
}
5591
}
5692

57-
const deleteIndex = async ({ name: indexUid }) => {
58-
const res = await request(`/${pluginId}/indexes/${indexUid}/`, {
93+
const removeCollection = async ({ name: collection }) => {
94+
const res = await request(`/${pluginId}/indexes/${collection}/`, {
5995
method: 'DELETE'
6096
})
6197
if (res.error) errorNotifications(res)
62-
else successNotification({ message: `${indexUid} collection is removed from MeiliSearch` })
98+
else successNotification({ message: `${collection} collection is removed from MeiliSearch!`, duration: 4000 })
6399
}
64100

65101
const addOrRemoveCollection = async (row) => {
66-
if (row._isChecked) await deleteIndex(row)
67-
else await addCollectionToMeiliSearch(row)
102+
if (row._isChecked) await removeCollection(row)
103+
else await addCollection(row)
68104
setUpdatedCollections(false)
69105
}
70106

@@ -74,17 +110,49 @@ const Collections = ({ updateCredentials }) => {
74110
})
75111
if (error) errorNotifications(res)
76112
else {
113+
const reloadNeeded = (indexed, hooked) => {
114+
if ((indexed && !hooked) || (!indexed && hooked)) {
115+
return 'Reload needed'
116+
} else if (indexed && hooked) {
117+
return 'Active'
118+
} else {
119+
return ''
120+
}
121+
}
122+
77123
const colStatus = collections.map(col => (
78124
{
79125
...col,
126+
status: (col.indexed) ? 'Indexed In MeiliSearch' : 'Not in MeiliSearch',
127+
hooked: reloadNeeded(col.indexed, col.hooked),
80128
_isChecked: col.indexed
81129
}
82130
))
131+
const reloading = colStatus.find(col => col.hooked === 'Reload needed')
132+
setNeedReload(reloading)
83133
setCollectionsList(colStatus)
84134
setUpdatedCollections(true)
85135
}
86136
}
87137

138+
const reload = async () => {
139+
try {
140+
strapi.lockApp({ enabled: true })
141+
const { error, ...res } = await request(`/${pluginId}/reload`, {
142+
method: 'GET'
143+
}, true)
144+
if (error) {
145+
errorNotifications(res)
146+
strapi.unlockApp()
147+
} else {
148+
window.location.reload()
149+
}
150+
} catch (err) {
151+
strapi.unlockApp()
152+
errorNotifications({ message: 'Could not reload the server' })
153+
}
154+
}
155+
88156
useEffect(() => {
89157
setUpdatedCollections(false)
90158
}, [updateCredentials])
@@ -104,7 +172,27 @@ const Collections = ({ updateCredentials }) => {
104172
onSelect={(row) => {
105173
addOrRemoveCollection(row)
106174
}}
175+
rowLinks={[
176+
{
177+
icon: <UpdateButton forwardedAs='span'>Update</UpdateButton>,
178+
onClick: data => {
179+
updateCollections({ collection: data.name })
180+
}
181+
}
182+
]}
107183
/>
184+
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
185+
{
186+
needReload && <Button
187+
color="delete"
188+
className="reload_button"
189+
onClick={() => { reload() }}
190+
style={{ marginTop: '20px' }}
191+
>
192+
Reload Server
193+
</Button>
194+
}
195+
</div>
108196
</Wrapper>
109197
</div>
110198
)

admin/src/utils/notifications.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ export function errorNotifications ({ message, link }) {
33
title: 'Operation on MeiliSearch failed',
44
type: 'warning',
55
message: message,
6-
...(link ? { link: { url: link, label: 'more information' } } : {}),
7-
timeout: 4000
6+
...(link && { link: { url: link, label: 'learn more' } }),
7+
blockTransition: true // The user has to close the error notification manually
88
})
99
}
1010

11-
export function successNotification ({ message }) {
11+
export function successNotification ({ message, duration = 4000, link }) {
1212
strapi.notification.toggle({
1313
type: 'success',
1414
message: message,
15-
timeout: 4000
15+
...(link && { link: { url: link, label: 'learn more' } }),
16+
timeout: duration
1617
})
1718
}

assets/collections.png

41.3 KB
Loading

assets/credentials.png

41.2 KB
Loading

assets/left_navbar.png

22.4 KB
Loading

config/functions/bootstrap.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use strict'
2+
3+
/**
4+
* An asynchronous bootstrap function that runs before
5+
* your application gets started.
6+
*
7+
* This gives you an opportunity to set up your data model,
8+
* run jobs, or perform some special logic.
9+
*
10+
* See more details here: https://strapi.io/documentation/developer-docs/latest/concepts/configurations.html#bootstrap
11+
*/
12+
13+
const meilisearch = {
14+
http: (client) => strapi.plugins.meilisearch.services.http(client),
15+
client: (credentials) => strapi.plugins.meilisearch.services.client(credentials),
16+
store: () => strapi.plugins.meilisearch.services.store,
17+
lifecycles: () => strapi.plugins.meilisearch.services.lifecycles
18+
}
19+
20+
async function getClient (credentials) {
21+
const client = meilisearch.client(credentials)
22+
return await meilisearch.http(client)
23+
}
24+
25+
async function getIndexes (client) {
26+
try {
27+
return client.getIndexes()
28+
} catch (e) {
29+
return []
30+
}
31+
}
32+
33+
async function getCredentials () {
34+
const store = await meilisearch.store()
35+
const apiKey = await store.getStoreKey('meilisearch_api_key')
36+
const host = await store.getStoreKey('meilisearch_host')
37+
return { apiKey, host }
38+
}
39+
40+
function addHookedCollectionsToStore ({ store, collections }) {
41+
store.set({
42+
key: 'meilisearch_hooked',
43+
value: collections
44+
})
45+
}
46+
47+
async function getHookedCollectionsFromStore ({ store }) {
48+
return store.get({ key: 'meilisearch_hooked' })
49+
}
50+
51+
function addLifecycles ({ client, collections }) {
52+
// Add lifecyles
53+
collections.map(collection => {
54+
const model = strapi.models[collection]
55+
const meilisearchLifecycles = Object.keys(meilisearch.lifecycles())
56+
model.lifecycles = model.lifecycles || {}
57+
58+
meilisearchLifecycles.map(lifecycleName => {
59+
const fn = model.lifecycles[lifecycleName] || (() => {})
60+
model.lifecycles[lifecycleName] = (data) => {
61+
fn(data)
62+
meilisearch.lifecycles()[lifecycleName](data, collection, client)
63+
}
64+
})
65+
})
66+
}
67+
68+
async function initHooks (store) {
69+
try {
70+
const credentials = await getCredentials()
71+
const getHookedCollections = await getHookedCollectionsFromStore({ store })
72+
// If hooked collection is not found it means that no MeiliSearch
73+
// Indexes are to be hooked. Meaning we do not have to add any lifecycle.
74+
if (credentials.host && getHookedCollections) {
75+
const client = await getClient(credentials)
76+
// get list of indexes in MeiliSearch Instance
77+
const indexes = (await getIndexes(client)).map(index => index.uid)
78+
79+
// Collections in Strapi
80+
const models = strapi.models
81+
82+
// get list of Indexes In MeilISearch that are Collections in Strapi
83+
const indexedCollections = Object.keys(models).filter(model => indexes.includes(model))
84+
addLifecycles({
85+
collections: indexedCollections,
86+
client
87+
})
88+
addHookedCollectionsToStore({
89+
collections: indexedCollections,
90+
store
91+
})
92+
}
93+
94+
// Add collections to hooked store
95+
} catch (e) {
96+
console.error(e)
97+
}
98+
}
99+
100+
module.exports = async () => {
101+
const store = strapi.store({
102+
environment: strapi.config.environment,
103+
type: 'plugin',
104+
name: 'meilisearch_store'
105+
})
106+
strapi.plugins.meilisearch.store = store
107+
108+
await initHooks(store)
109+
}

config/functions/cron.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict'
2+
3+
/**
4+
* Cron config that gives you an opportunity
5+
* to run scheduled jobs.
6+
*
7+
* The cron format consists of:
8+
* [SECOND (optional)] [MINUTE] [HOUR] [DAY OF MONTH] [MONTH OF YEAR] [DAY OF WEEK]
9+
*
10+
* See more details here: https://strapi.io/documentation/developer-docs/latest/concepts/configurations.html#cron-tasks
11+
*/
12+
13+
module.exports = {
14+
/**
15+
* Simple example.
16+
* Every monday at 1am.
17+
*/
18+
// '0 1 * * 1': () => {
19+
//
20+
// }
21+
}

0 commit comments

Comments
 (0)