feat: add ability to search images and tags by name (#177)

This commit is contained in:
Jones Magloire 2021-04-05 00:01:54 +02:00 committed by GitHub
commit 83f15aa267
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 91 additions and 17 deletions

View file

@ -16,7 +16,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<catalog-element> <catalog-element>
<!-- Begin of tag --> <!-- Begin of tag -->
<div class="content"> <div class="content"
if="{!props.filterResults || state.nImages > 0 || matchSearch(props.filterResults, state.image)}">
<material-card class="list highlight" expanded="{state.expanded}" onclick="{ onClick }"> <material-card class="list highlight" expanded="{state.expanded}" onclick="{ onClick }">
<material-waves onmousedown="{this.triggerLaunch}" center="true" color="#ddd" <material-waves onmousedown="{this.triggerLaunch}" center="true" color="#ddd"
setLaunchListener="{ setLaunchListener }" /> setLaunchListener="{ setLaunchListener }" />
@ -24,17 +25,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<i class="material-icons">send</i> <i class="material-icons">send</i>
{ state.image || state.repo } { state.image || state.repo }
<div if="{state.images}" class="item-count right"> <div if="{state.images}" class="item-count right">
{ state.images.length } images { state.nImages } images
<i class="material-icons animated {state.expanded ? 'expanded' : ''}">expand_more</i> <i class="material-icons animated {state.expanded ? 'expanded' : ''}">expand_more</i>
</div> </div>
</span> </span>
</material-card> </material-card>
<catalog-element if="{ state.images }" <catalog-element if="{ state.images }" filter-results="{ props.filterResults }"
class="animated {!state.expanded ? 'hide' : ''} {state.expanding ? 'expanding' : ''}" class="animated {!state.expanded && !props.filterResults ? 'hide' : ''} {state.expanding ? 'expanding' : ''}"
each="{item in state.images}" item="{ item }" /> each="{item in state.images}" item="{ item }" />
</div> </div>
<script> <script>
import router from '../../scripts/router'; import router from '../../scripts/router';
import {
matchSearch
} from '../search-bar.riot';
export default { export default {
onBeforeMount(props, state) { onBeforeMount(props, state) {
@ -45,6 +49,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
} else if (props.item.images && props.item.repo) { } else if (props.item.images && props.item.repo) {
state.images = props.item.images; state.images = props.item.images;
state.repo = props.item.repo; state.repo = props.item.repo;
state.nImages = props.item.images.length;
}
},
onBeforeUpdate(props, state) {
if (props.filterResults && state.images) {
state.nImages = state.images.filter(image => matchSearch(props.filterResults, image)).length;
} else {
state.nImages = state.images && state.images.length;
} }
}, },
onClick() { onClick() {
@ -65,7 +77,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}, },
setLaunchListener(cb) { setLaunchListener(cb) {
this.triggerLaunch = cb; this.triggerLaunch = cb;
} },
matchSearch
} }
</script> </script>
<!-- End of tag --> <!-- End of tag -->

View file

@ -26,7 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div if="{ !state.loadend }" class="spinner-wrapper"> <div if="{ !state.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner> <material-spinner></material-spinner>
</div> </div>
<catalog-element each="{ item in state.repositories }" item="{ item }" /> <catalog-element each="{ item in state.repositories }" item="{ item }" filter-results="{ props.filterResults }"/>
<script> <script>
import CatalogElement from './catalog-element.riot' import CatalogElement from './catalog-element.riot'
import { import {

View file

@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<header> <header>
<material-navbar> <material-navbar>
<div class="logo">Docker Registry UI</div> <div class="logo">Docker Registry UI</div>
<search-bar on-search="{ onSearch }"></search-bar>
<dialogs-menu if="{props.singleRegistry !== 'true'}" on-notify="{ notifySnackbar }" <dialogs-menu if="{props.singleRegistry !== 'true'}" on-notify="{ notifySnackbar }"
on-server-change="{ onServerChange }"></dialogs-menu> on-server-change="{ onServerChange }"></dialogs-menu>
</material-navbar> </material-navbar>
@ -26,12 +27,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<router base="#!"> <router base="#!">
<route path="{baseRoute}"> <route path="{baseRoute}">
<catalog registry-url="{ state.registryUrl }" registry-name="{ state.name }" <catalog registry-url="{ state.registryUrl }" registry-name="{ state.name }"
catalog-elements-limit="{ state.catalogElementsLimit }" on-notify="{ notifySnackbar }" /> catalog-elements-limit="{ state.catalogElementsLimit }" on-notify="{ notifySnackbar }"
filter-results="{ state.filter }" />
</route> </route>
<route path="{baseRoute}taglist/(.*)"> <route path="{baseRoute}taglist/(.*)">
<tag-list registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }" <tag-list registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
image="{ router.getTagListImage() }" show-content-digest="{props.showContentDigest}" image="{ router.getTagListImage() }" show-content-digest="{props.showContentDigest}"
is-image-remove-activated="{props.isImageRemoveActivated}" on-notify="{ notifySnackbar }"></tag-list> is-image-remove-activated="{props.isImageRemoveActivated}" on-notify="{ notifySnackbar }"
filter-results="{ state.filter }"></tag-list>
</route> </route>
<route path="{baseRoute}taghistory/(.*)"> <route path="{baseRoute}taghistory/(.*)">
<tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }" <tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
@ -66,6 +69,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import TagList from './tag-list/tag-list.riot'; import TagList from './tag-list/tag-list.riot';
import TagHistory from './tag-history/tag-history.riot'; import TagHistory from './tag-history/tag-history.riot';
import DialogsMenu from './dialogs/dialogs-menu.riot'; import DialogsMenu from './dialogs/dialogs-menu.riot';
import SearchBar from './search-bar.riot'
import { import {
stripHttps, stripHttps,
getRegistryServers getRegistryServers
@ -78,6 +82,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
TagList, TagList,
TagHistory, TagHistory,
DialogsMenu, DialogsMenu,
SearchBar,
Router, Router,
Route Route
}, },
@ -121,6 +126,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}); });
} }
}, },
onSearch(value) {
this.update({
filter: value
})
},
baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)', baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)',
router, router,
version version

View file

@ -0,0 +1,48 @@
<search-bar>
<material-input placeholder="Search in page"></material-input>
<script>
import {
router
} from '@riotjs/route';
export default {
onMounted(props, state) {
const input = this.$('input');
let value = '';
const notify = () => {
if (value !== input.value) {
props.onSearch(input.value.toLowerCase())
}
value = input.value;
}
input.addEventListener('keyup', notify);
router.on.value(() => {
input.value = '';
notify();
})
}
}
export function matchSearch(search, value) {
return !search || (value && value.toLowerCase().indexOf(search) >= 0);
}
</script>
<style>
:host material-input {
position: absolute;
top: 0em;
right: 64px;
max-width: 20%;
}
@media screen and (max-width: 400px) {
:host material-input {
display: none;
}
}
:host material-input input {
color: #fff;
}
</style>
</search-bar>

View file

@ -39,7 +39,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }" <tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }"
show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}" show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}"
onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }" onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }"></tag-table> on-notify="{ props.onNotify }" filter-results="{ props.filterResults }"></tag-table>
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination> <pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
@ -84,15 +84,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
const self = this; const self = this;
const oReq = new Http(); const oReq = new Http();
oReq.addEventListener('load', function () { oReq.addEventListener('load', function () {
state.tags = [];
if (this.status == 200) { if (this.status == 200) {
const tags = JSON.parse(this.responseText).tags || []; const tags = (JSON.parse(this.responseText).tags || [])
state.tags = tags.map(function (tag) { .map(tag => new DockerImage(props.image, tag, null, props.registryUrl, props.onNotify))
return new DockerImage(props.image, tag, null, props.registryUrl, props.onNotify); .sort(compare);
}).sort(compare);
window.requestAnimationFrame(self.onResize); window.requestAnimationFrame(self.onResize);
self.update({ self.update({
page: Math.min(state.page, getNumPages(state.tags)) page: Math.min(state.page, getNumPages(tags)),
tags
}) })
} else if (this.status == 404) { } else if (this.status == 404) {
self.props.onNotify('Server not found', true); self.props.onNotify('Server not found', true);

View file

@ -40,7 +40,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr each="{ image in getPage(props.tags, props.page) }"> <tr each="{ image in getPage(props.tags, props.page) }" if="{ matchSearch(props.filterResults, image.tag) }">
<td class="creation-date"> <td class="creation-date">
<image-date image="{ image }" /> <image-date image="{ image }" />
</td> </td>
@ -85,6 +85,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import RemoveImage, { import RemoveImage, {
deleteImage deleteImage
} from './remove-image.riot'; } from './remove-image.riot';
import {
matchSearch
} from '../search-bar.riot';
export default { export default {
components: { components: {
ImageDate, ImageDate,
@ -135,7 +138,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
toDelete: this.state.toDelete toDelete: this.state.toDelete
}) })
}, },
getPage getPage,
matchSearch
} }
</script> </script>
</tag-table> </tag-table>