mirror of
https://github.com/Joxit/docker-registry-ui.git
synced 2025-04-28 16:09:54 +03:00
feat: add ability to search images and tags by name (#177)
This commit is contained in:
commit
83f15aa267
6 changed files with 91 additions and 17 deletions
|
@ -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 -->
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
48
src/components/search-bar.riot
Normal file
48
src/components/search-bar.riot
Normal 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>
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue