Manager: Show "nothing rendered yet" image in job details
Show a "nothing rendered yet" image in the job details when there is no last-rendered image yet.
This commit is contained in:
parent
56463fa3ec
commit
2457a63518
@ -131,6 +131,9 @@ type LastRendered interface {
|
|||||||
|
|
||||||
// ThumbSpecs returns the thumbnail specifications.
|
// ThumbSpecs returns the thumbnail specifications.
|
||||||
ThumbSpecs() []last_rendered.Thumbspec
|
ThumbSpecs() []last_rendered.Thumbspec
|
||||||
|
|
||||||
|
// JobHasImage returns true only if the job actually has a last-rendered image.
|
||||||
|
JobHasImage(jobUUID string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocalStorage handles the storage organisation of local files like the last-rendered images.
|
// LocalStorage handles the storage organisation of local files like the last-rendered images.
|
||||||
|
@ -317,6 +317,10 @@ func (f *Flamenco) FetchJobLastRenderedInfo(e echo.Context, jobID string) error
|
|||||||
return sendAPIError(e, http.StatusBadRequest, "job ID should be a UUID")
|
return sendAPIError(e, http.StatusBadRequest, "job ID should be a UUID")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !f.lastRender.JobHasImage(jobID) {
|
||||||
|
return e.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
logger := requestLogger(e)
|
logger := requestLogger(e)
|
||||||
info, err := f.lastRenderedInfoForJob(logger, jobID)
|
info, err := f.lastRenderedInfoForJob(logger, jobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.blender.org/flamenco/internal/manager/job_compilers"
|
"git.blender.org/flamenco/internal/manager/job_compilers"
|
||||||
|
"git.blender.org/flamenco/internal/manager/last_rendered"
|
||||||
"git.blender.org/flamenco/internal/manager/persistence"
|
"git.blender.org/flamenco/internal/manager/persistence"
|
||||||
"git.blender.org/flamenco/pkg/api"
|
"git.blender.org/flamenco/pkg/api"
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
@ -312,3 +313,43 @@ func TestFetchTaskLogTail(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assertResponseNoContent(t, echoCtx)
|
assertResponseNoContent(t, echoCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetchJobLastRenderedInfo(t *testing.T) {
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
defer mockCtrl.Finish()
|
||||||
|
|
||||||
|
mf := newMockedFlamenco(mockCtrl)
|
||||||
|
|
||||||
|
jobID := "18a9b096-d77e-438c-9be2-74397038298b"
|
||||||
|
|
||||||
|
{
|
||||||
|
// Last-rendered image has been processed.
|
||||||
|
mf.lastRender.EXPECT().JobHasImage(jobID).Return(true)
|
||||||
|
mf.lastRender.EXPECT().PathForJob(jobID).Return("/absolute/path/to/local/job/dir")
|
||||||
|
mf.localStorage.EXPECT().RelPath("/absolute/path/to/local/job/dir").Return("relative/path", nil)
|
||||||
|
mf.lastRender.EXPECT().ThumbSpecs().Return([]last_rendered.Thumbspec{
|
||||||
|
{Filename: "das grosses potaat.jpg"},
|
||||||
|
{Filename: "invisibru.jpg"},
|
||||||
|
})
|
||||||
|
|
||||||
|
echoCtx := mf.prepareMockedRequest(nil)
|
||||||
|
err := mf.flamenco.FetchJobLastRenderedInfo(echoCtx, jobID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expectBody := api.JobLastRenderedImageInfo{
|
||||||
|
Base: "/job-files/relative/path",
|
||||||
|
Suffixes: []string{"das grosses potaat.jpg", "invisibru.jpg"},
|
||||||
|
}
|
||||||
|
assertResponseJSON(t, echoCtx, http.StatusOK, expectBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// No last-rendered image exists.
|
||||||
|
mf.lastRender.EXPECT().JobHasImage(jobID).Return(false)
|
||||||
|
|
||||||
|
echoCtx := mf.prepareMockedRequest(nil)
|
||||||
|
err := mf.flamenco.FetchJobLastRenderedInfo(echoCtx, jobID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assertResponseNoContent(t, echoCtx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -867,6 +867,20 @@ func (m *MockLastRendered) EXPECT() *MockLastRenderedMockRecorder {
|
|||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JobHasImage mocks base method.
|
||||||
|
func (m *MockLastRendered) JobHasImage(arg0 string) bool {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "JobHasImage", arg0)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// JobHasImage indicates an expected call of JobHasImage.
|
||||||
|
func (mr *MockLastRenderedMockRecorder) JobHasImage(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JobHasImage", reflect.TypeOf((*MockLastRendered)(nil).JobHasImage), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
// PathForJob mocks base method.
|
// PathForJob mocks base method.
|
||||||
func (m *MockLastRendered) PathForJob(arg0 string) string {
|
func (m *MockLastRendered) PathForJob(arg0 string) string {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -5,6 +5,8 @@ package last_rendered
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
@ -110,6 +112,26 @@ func (lrp *LastRenderedProcessor) PathForJob(jobUUID string) string {
|
|||||||
return lrp.storage.ForJob(jobUUID)
|
return lrp.storage.ForJob(jobUUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JobHasImage returns true only if the job actually has a last-rendered image.
|
||||||
|
// Only the lowest-resolution image is tested for. Since images are processed in
|
||||||
|
// order, existence of the last one should imply existence of all of them.
|
||||||
|
func (lrp *LastRenderedProcessor) JobHasImage(jobUUID string) bool {
|
||||||
|
dirPath := lrp.PathForJob(jobUUID)
|
||||||
|
filename := thumbnails[len(thumbnails)-1].Filename
|
||||||
|
path := filepath.Join(dirPath, filename)
|
||||||
|
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
return true
|
||||||
|
case errors.Is(err, fs.ErrNotExist):
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
log.Warn().Err(err).Str("path", path).Msg("last-rendered: unexpected error checking file for existence")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ThumbSpecs returns the thumbnail specifications.
|
// ThumbSpecs returns the thumbnail specifications.
|
||||||
func (lrp *LastRenderedProcessor) ThumbSpecs() []Thumbspec {
|
func (lrp *LastRenderedProcessor) ThumbSpecs() []Thumbspec {
|
||||||
// Return a copy so modification of the returned slice won't affect the global
|
// Return a copy so modification of the returned slice won't affect the global
|
||||||
|
67
web/app/public/nothing-rendered-yet.svg
Normal file
67
web/app/public/nothing-rendered-yet.svg
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="200"
|
||||||
|
height="112"
|
||||||
|
viewBox="0 0 52.916666 29.633335"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
|
||||||
|
sodipodi:docname="nothing-rendered-yet.svg">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#353535"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="1"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="3.959798"
|
||||||
|
inkscape:cx="105.3154"
|
||||||
|
inkscape:cy="96.674204"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
showgrid="false"
|
||||||
|
units="px"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1343"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata5">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.64583px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;"
|
||||||
|
x="26.425261"
|
||||||
|
y="15.504582"
|
||||||
|
id="text837"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan835"
|
||||||
|
x="26.425261"
|
||||||
|
y="15.504582"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Noto Sans';-inkscape-font-specification:'Noto Sans';text-align:center;text-anchor:middle;stroke-width:0.264583px;fill:#ffffff;fill-opacity:1;">Nothing rendered yet...</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.6 KiB |
@ -1,11 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { reactive, ref, watch } from 'vue'
|
||||||
import { api } from '@/urls';
|
import { api } from '@/urls';
|
||||||
import { JobsApi, JobLastRenderedImageInfo, SocketIOLastRenderedUpdate } from '@/manager-api';
|
import { JobsApi, JobLastRenderedImageInfo, SocketIOLastRenderedUpdate } from '@/manager-api';
|
||||||
import { apiClient } from '@/stores/api-query-count';
|
import { apiClient } from '@/stores/api-query-count';
|
||||||
|
|
||||||
const props = defineProps(['jobID']);
|
const props = defineProps(['jobID']);
|
||||||
const imageURL = ref('');
|
const imageURL = ref('');
|
||||||
|
const cssClasses = reactive({
|
||||||
|
lastRendered: true,
|
||||||
|
nothingRenderedYet: true,
|
||||||
|
})
|
||||||
|
|
||||||
const jobsApi = new JobsApi(apiClient);
|
const jobsApi = new JobsApi(apiClient);
|
||||||
|
|
||||||
@ -22,6 +26,14 @@ function fetchImageURL(jobID) {
|
|||||||
* @param {JobLastRenderedImageInfo} thumbnailInfo
|
* @param {JobLastRenderedImageInfo} thumbnailInfo
|
||||||
*/
|
*/
|
||||||
function setImageURL(thumbnailInfo) {
|
function setImageURL(thumbnailInfo) {
|
||||||
|
if (thumbnailInfo == null) {
|
||||||
|
// This indicates that there is no last-rendered image.
|
||||||
|
// Default to a hard-coded 'nothing to be seen here, move along' image.
|
||||||
|
imageURL.value = "/nothing-rendered-yet.svg";
|
||||||
|
cssClasses.nothingRenderedYet = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set the image URL to something appropriate.
|
// Set the image URL to something appropriate.
|
||||||
for (let suffix of thumbnailInfo.suffixes) {
|
for (let suffix of thumbnailInfo.suffixes) {
|
||||||
if (!suffix.includes("-tiny")) continue;
|
if (!suffix.includes("-tiny")) continue;
|
||||||
@ -32,6 +44,7 @@ function setImageURL(thumbnailInfo) {
|
|||||||
imageURL.value = url.toString();
|
imageURL.value = url.toString();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
cssClasses.nothingRenderedYet = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,7 +77,7 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="imageURL != ''" class="lastRendered">
|
<div v-if="imageURL != ''" :class="cssClasses">
|
||||||
<img :src="imageURL" alt="Last-rendered image for this job">
|
<img :src="imageURL" alt="Last-rendered image for this job">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -75,4 +88,8 @@ defineExpose({
|
|||||||
height: 112px;
|
height: 112px;
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lastRendered.nothingRenderedYet {
|
||||||
|
outline: thin dotted var(--color-text-hint);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user