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:
Sybren A. Stüvel 2022-06-30 19:20:19 +02:00
parent 56463fa3ec
commit 2457a63518
7 changed files with 170 additions and 2 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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

View 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

View File

@ -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>