Job status change system for SocketIO broadcasts
Not fully tested yet.
This commit is contained in:
parent
c875745bd4
commit
0c0df41f5d
@ -10,7 +10,7 @@
|
||||
"""
|
||||
|
||||
|
||||
__version__ = "781f1d93-dirty"
|
||||
__version__ = "c875745b-dirty"
|
||||
|
||||
# import ApiClient
|
||||
from flamenco.manager.api_client import ApiClient
|
||||
|
@ -76,7 +76,7 @@ class ApiClient(object):
|
||||
self.default_headers[header_name] = header_value
|
||||
self.cookie = cookie
|
||||
# Set default User-Agent.
|
||||
self.user_agent = 'Flamenco/781f1d93-dirty (Blender add-on)'
|
||||
self.user_agent = 'Flamenco/c875745b-dirty (Blender add-on)'
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
@ -404,7 +404,7 @@ conf = flamenco.manager.Configuration(
|
||||
"OS: {env}\n"\
|
||||
"Python Version: {pyversion}\n"\
|
||||
"Version of the API: 1.0.0\n"\
|
||||
"SDK Package Version: 781f1d93-dirty".\
|
||||
"SDK Package Version: c875745b-dirty".\
|
||||
format(env=sys.platform, pyversion=sys.version)
|
||||
|
||||
def get_host_settings(self):
|
||||
|
15
addon/flamenco/manager/docs/JobUpdate.md
Normal file
15
addon/flamenco/manager/docs/JobUpdate.md
Normal file
@ -0,0 +1,15 @@
|
||||
# JobUpdate
|
||||
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**id** | **str** | UUID of the Job |
|
||||
**updated** | **datetime** | Timestamp of last update |
|
||||
**status** | [**JobStatus**](JobStatus.md) | |
|
||||
**previous_status** | [**JobStatus**](JobStatus.md) | | [optional]
|
||||
**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
283
addon/flamenco/manager/model/job_update.py
Normal file
283
addon/flamenco/manager/model/job_update.py
Normal file
@ -0,0 +1,283 @@
|
||||
"""
|
||||
Flamenco manager
|
||||
|
||||
Render Farm manager API # noqa: E501
|
||||
|
||||
The version of the OpenAPI document: 1.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
|
||||
import re # noqa: F401
|
||||
import sys # noqa: F401
|
||||
|
||||
from flamenco.manager.model_utils import ( # noqa: F401
|
||||
ApiTypeError,
|
||||
ModelComposed,
|
||||
ModelNormal,
|
||||
ModelSimple,
|
||||
cached_property,
|
||||
change_keys_js_to_python,
|
||||
convert_js_args_to_python_args,
|
||||
date,
|
||||
datetime,
|
||||
file_type,
|
||||
none_type,
|
||||
validate_get_composed_info,
|
||||
OpenApiModel
|
||||
)
|
||||
from flamenco.manager.exceptions import ApiAttributeError
|
||||
|
||||
|
||||
def lazy_import():
|
||||
from flamenco.manager.model.job_status import JobStatus
|
||||
globals()['JobStatus'] = JobStatus
|
||||
|
||||
|
||||
class JobUpdate(ModelNormal):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator.
|
||||
Ref: https://openapi-generator.tech
|
||||
|
||||
Do not edit the class manually.
|
||||
|
||||
Attributes:
|
||||
allowed_values (dict): The key is the tuple path to the attribute
|
||||
and the for var_name this is (var_name,). The value is a dict
|
||||
with a capitalized key describing the allowed value and an allowed
|
||||
value. These dicts store the allowed enum values.
|
||||
attribute_map (dict): The key is attribute name
|
||||
and the value is json key in definition.
|
||||
discriminator_value_class_map (dict): A dict to go from the discriminator
|
||||
variable value to the discriminator class name.
|
||||
validations (dict): The key is the tuple path to the attribute
|
||||
and the for var_name this is (var_name,). The value is a dict
|
||||
that stores validations for max_length, min_length, max_items,
|
||||
min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum,
|
||||
inclusive_minimum, and regex.
|
||||
additional_properties_type (tuple): A tuple of classes accepted
|
||||
as additional properties values.
|
||||
"""
|
||||
|
||||
allowed_values = {
|
||||
}
|
||||
|
||||
validations = {
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def additional_properties_type():
|
||||
"""
|
||||
This must be a method because a model may have properties that are
|
||||
of type self, this must run after the class is loaded
|
||||
"""
|
||||
lazy_import()
|
||||
return (bool, date, datetime, dict, float, int, list, str, none_type,) # noqa: E501
|
||||
|
||||
_nullable = False
|
||||
|
||||
@cached_property
|
||||
def openapi_types():
|
||||
"""
|
||||
This must be a method because a model may have properties that are
|
||||
of type self, this must run after the class is loaded
|
||||
|
||||
Returns
|
||||
openapi_types (dict): The key is attribute name
|
||||
and the value is attribute type.
|
||||
"""
|
||||
lazy_import()
|
||||
return {
|
||||
'id': (str,), # noqa: E501
|
||||
'updated': (datetime,), # noqa: E501
|
||||
'status': (JobStatus,), # noqa: E501
|
||||
'previous_status': (JobStatus,), # noqa: E501
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def discriminator():
|
||||
return None
|
||||
|
||||
|
||||
attribute_map = {
|
||||
'id': 'id', # noqa: E501
|
||||
'updated': 'updated', # noqa: E501
|
||||
'status': 'status', # noqa: E501
|
||||
'previous_status': 'previous_status', # noqa: E501
|
||||
}
|
||||
|
||||
read_only_vars = {
|
||||
}
|
||||
|
||||
_composed_schemas = {}
|
||||
|
||||
@classmethod
|
||||
@convert_js_args_to_python_args
|
||||
def _from_openapi_data(cls, id, updated, status, *args, **kwargs): # noqa: E501
|
||||
"""JobUpdate - a model defined in OpenAPI
|
||||
|
||||
Args:
|
||||
id (str): UUID of the Job
|
||||
updated (datetime): Timestamp of last update
|
||||
status (JobStatus):
|
||||
|
||||
Keyword Args:
|
||||
_check_type (bool): if True, values for parameters in openapi_types
|
||||
will be type checked and a TypeError will be
|
||||
raised if the wrong type is input.
|
||||
Defaults to True
|
||||
_path_to_item (tuple/list): This is a list of keys or values to
|
||||
drill down to the model in received_data
|
||||
when deserializing a response
|
||||
_spec_property_naming (bool): True if the variable names in the input data
|
||||
are serialized names, as specified in the OpenAPI document.
|
||||
False if the variable names in the input data
|
||||
are pythonic names, e.g. snake case (default)
|
||||
_configuration (Configuration): the instance to use when
|
||||
deserializing a file_type parameter.
|
||||
If passed, type conversion is attempted
|
||||
If omitted no type conversion is done.
|
||||
_visited_composed_classes (tuple): This stores a tuple of
|
||||
classes that we have traveled through so that
|
||||
if we see that class again we will not use its
|
||||
discriminator again.
|
||||
When traveling through a discriminator, the
|
||||
composed schema that is
|
||||
is traveled through is added to this set.
|
||||
For example if Animal has a discriminator
|
||||
petType and we pass in "Dog", and the class Dog
|
||||
allOf includes Animal, we move through Animal
|
||||
once using the discriminator, and pick Dog.
|
||||
Then in Dog, we will make an instance of the
|
||||
Animal class but this time we won't travel
|
||||
through its discriminator because we passed in
|
||||
_visited_composed_classes = (Animal,)
|
||||
previous_status (JobStatus): [optional] # noqa: E501
|
||||
"""
|
||||
|
||||
_check_type = kwargs.pop('_check_type', True)
|
||||
_spec_property_naming = kwargs.pop('_spec_property_naming', False)
|
||||
_path_to_item = kwargs.pop('_path_to_item', ())
|
||||
_configuration = kwargs.pop('_configuration', None)
|
||||
_visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
|
||||
|
||||
self = super(OpenApiModel, cls).__new__(cls)
|
||||
|
||||
if args:
|
||||
raise ApiTypeError(
|
||||
"Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
|
||||
args,
|
||||
self.__class__.__name__,
|
||||
),
|
||||
path_to_item=_path_to_item,
|
||||
valid_classes=(self.__class__,),
|
||||
)
|
||||
|
||||
self._data_store = {}
|
||||
self._check_type = _check_type
|
||||
self._spec_property_naming = _spec_property_naming
|
||||
self._path_to_item = _path_to_item
|
||||
self._configuration = _configuration
|
||||
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
|
||||
|
||||
self.id = id
|
||||
self.updated = updated
|
||||
self.status = status
|
||||
for var_name, var_value in kwargs.items():
|
||||
if var_name not in self.attribute_map and \
|
||||
self._configuration is not None and \
|
||||
self._configuration.discard_unknown_keys and \
|
||||
self.additional_properties_type is None:
|
||||
# discard variable.
|
||||
continue
|
||||
setattr(self, var_name, var_value)
|
||||
return self
|
||||
|
||||
required_properties = set([
|
||||
'_data_store',
|
||||
'_check_type',
|
||||
'_spec_property_naming',
|
||||
'_path_to_item',
|
||||
'_configuration',
|
||||
'_visited_composed_classes',
|
||||
])
|
||||
|
||||
@convert_js_args_to_python_args
|
||||
def __init__(self, id, updated, status, *args, **kwargs): # noqa: E501
|
||||
"""JobUpdate - a model defined in OpenAPI
|
||||
|
||||
Args:
|
||||
id (str): UUID of the Job
|
||||
updated (datetime): Timestamp of last update
|
||||
status (JobStatus):
|
||||
|
||||
Keyword Args:
|
||||
_check_type (bool): if True, values for parameters in openapi_types
|
||||
will be type checked and a TypeError will be
|
||||
raised if the wrong type is input.
|
||||
Defaults to True
|
||||
_path_to_item (tuple/list): This is a list of keys or values to
|
||||
drill down to the model in received_data
|
||||
when deserializing a response
|
||||
_spec_property_naming (bool): True if the variable names in the input data
|
||||
are serialized names, as specified in the OpenAPI document.
|
||||
False if the variable names in the input data
|
||||
are pythonic names, e.g. snake case (default)
|
||||
_configuration (Configuration): the instance to use when
|
||||
deserializing a file_type parameter.
|
||||
If passed, type conversion is attempted
|
||||
If omitted no type conversion is done.
|
||||
_visited_composed_classes (tuple): This stores a tuple of
|
||||
classes that we have traveled through so that
|
||||
if we see that class again we will not use its
|
||||
discriminator again.
|
||||
When traveling through a discriminator, the
|
||||
composed schema that is
|
||||
is traveled through is added to this set.
|
||||
For example if Animal has a discriminator
|
||||
petType and we pass in "Dog", and the class Dog
|
||||
allOf includes Animal, we move through Animal
|
||||
once using the discriminator, and pick Dog.
|
||||
Then in Dog, we will make an instance of the
|
||||
Animal class but this time we won't travel
|
||||
through its discriminator because we passed in
|
||||
_visited_composed_classes = (Animal,)
|
||||
previous_status (JobStatus): [optional] # noqa: E501
|
||||
"""
|
||||
|
||||
_check_type = kwargs.pop('_check_type', True)
|
||||
_spec_property_naming = kwargs.pop('_spec_property_naming', False)
|
||||
_path_to_item = kwargs.pop('_path_to_item', ())
|
||||
_configuration = kwargs.pop('_configuration', None)
|
||||
_visited_composed_classes = kwargs.pop('_visited_composed_classes', ())
|
||||
|
||||
if args:
|
||||
raise ApiTypeError(
|
||||
"Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % (
|
||||
args,
|
||||
self.__class__.__name__,
|
||||
),
|
||||
path_to_item=_path_to_item,
|
||||
valid_classes=(self.__class__,),
|
||||
)
|
||||
|
||||
self._data_store = {}
|
||||
self._check_type = _check_type
|
||||
self._spec_property_naming = _spec_property_naming
|
||||
self._path_to_item = _path_to_item
|
||||
self._configuration = _configuration
|
||||
self._visited_composed_classes = _visited_composed_classes + (self.__class__,)
|
||||
|
||||
self.id = id
|
||||
self.updated = updated
|
||||
self.status = status
|
||||
for var_name, var_value in kwargs.items():
|
||||
if var_name not in self.attribute_map and \
|
||||
self._configuration is not None and \
|
||||
self._configuration.discard_unknown_keys and \
|
||||
self.additional_properties_type is None:
|
||||
# discard variable.
|
||||
continue
|
||||
setattr(self, var_name, var_value)
|
||||
if var_name in self.read_only_vars:
|
||||
raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate "
|
||||
f"class with read only attributes.")
|
@ -23,6 +23,7 @@ from flamenco.manager.model.job_all_of import JobAllOf
|
||||
from flamenco.manager.model.job_metadata import JobMetadata
|
||||
from flamenco.manager.model.job_settings import JobSettings
|
||||
from flamenco.manager.model.job_status import JobStatus
|
||||
from flamenco.manager.model.job_update import JobUpdate
|
||||
from flamenco.manager.model.jobs_query import JobsQuery
|
||||
from flamenco.manager.model.jobs_query_result import JobsQueryResult
|
||||
from flamenco.manager.model.manager_configuration import ManagerConfiguration
|
||||
|
@ -4,7 +4,7 @@ Render Farm manager API
|
||||
The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.0.0
|
||||
- Package version: 781f1d93-dirty
|
||||
- Package version: c875745b-dirty
|
||||
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
|
||||
For more information, please visit [https://flamenco.io/](https://flamenco.io/)
|
||||
|
||||
@ -101,6 +101,7 @@ Class | Method | HTTP request | Description
|
||||
- [JobMetadata](flamenco/manager/docs/JobMetadata.md)
|
||||
- [JobSettings](flamenco/manager/docs/JobSettings.md)
|
||||
- [JobStatus](flamenco/manager/docs/JobStatus.md)
|
||||
- [JobUpdate](flamenco/manager/docs/JobUpdate.md)
|
||||
- [JobsQuery](flamenco/manager/docs/JobsQuery.md)
|
||||
- [JobsQueryResult](flamenco/manager/docs/JobsQueryResult.md)
|
||||
- [ManagerConfiguration](flamenco/manager/docs/ManagerConfiguration.md)
|
||||
|
@ -94,8 +94,8 @@ func main() {
|
||||
//
|
||||
// go persist.PeriodicMaintenanceLoop(mainCtx)
|
||||
|
||||
flamenco := buildFlamencoAPI(configService, persist)
|
||||
webUpdater := webupdates.New()
|
||||
flamenco := buildFlamencoAPI(configService, persist, webUpdater)
|
||||
e := buildWebService(flamenco, persist, ssdp, webUpdater)
|
||||
|
||||
installSignalHandler(mainCtxCancel)
|
||||
@ -132,14 +132,14 @@ func main() {
|
||||
log.Info().Msg("shutdown complete")
|
||||
}
|
||||
|
||||
func buildFlamencoAPI(configService *config.Service, persist *persistence.DB) api.ServerInterface {
|
||||
func buildFlamencoAPI(configService *config.Service, persist *persistence.DB, webUpdater *webupdates.BiDirComms) api.ServerInterface {
|
||||
timeService := clock.New()
|
||||
compiler, err := job_compilers.Load(timeService)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error loading job compilers")
|
||||
}
|
||||
logStorage := task_logs.NewStorage(configService.Get().TaskLogsPath)
|
||||
taskStateMachine := task_state_machine.NewStateMachine(persist)
|
||||
taskStateMachine := task_state_machine.NewStateMachine(persist, webUpdater)
|
||||
shamanServer := shaman.NewServer(configService.Get().Shaman, nil)
|
||||
flamenco := api_impl.NewFlamenco(compiler, persist, logStorage, configService, taskStateMachine, shamanServer)
|
||||
return flamenco
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"git.blender.org/flamenco/internal/manager/job_compilers"
|
||||
"git.blender.org/flamenco/internal/manager/persistence"
|
||||
"git.blender.org/flamenco/internal/manager/task_state_machine"
|
||||
"git.blender.org/flamenco/internal/manager/webupdates"
|
||||
"git.blender.org/flamenco/pkg/api"
|
||||
"git.blender.org/flamenco/pkg/shaman"
|
||||
"github.com/labstack/echo/v4"
|
||||
@ -66,6 +67,14 @@ type TaskStateMachine interface {
|
||||
// TaskStateMachine should be a subset of task_state_machine.StateMachine.
|
||||
var _ TaskStateMachine = (*task_state_machine.StateMachine)(nil)
|
||||
|
||||
type ChangeBroadcaster interface {
|
||||
// BroadcastNewJob sends a 'new job' notification to all SocketIO clients.
|
||||
BroadcastNewJob(jobUpdate api.JobUpdate)
|
||||
}
|
||||
|
||||
// ChangeBroadcaster should be a subset of webupdates.BiDirComms.
|
||||
var _ ChangeBroadcaster = (*webupdates.BiDirComms)(nil)
|
||||
|
||||
type JobCompiler interface {
|
||||
ListJobTypes() api.AvailableJobTypes
|
||||
Compile(ctx context.Context, job api.SubmittedJob) (*job_compilers.AuthoredJob, error)
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: git.blender.org/flamenco/internal/manager/task_state_machine (interfaces: PersistenceService)
|
||||
// Source: git.blender.org/flamenco/internal/manager/task_state_machine (interfaces: PersistenceService,ChangeBroadcaster)
|
||||
|
||||
// Package mocks is a generated GoMock package.
|
||||
package mocks
|
||||
@ -122,3 +122,38 @@ func (mr *MockPersistenceServiceMockRecorder) UpdateJobsTaskStatusesConditional(
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateJobsTaskStatusesConditional", reflect.TypeOf((*MockPersistenceService)(nil).UpdateJobsTaskStatusesConditional), arg0, arg1, arg2, arg3, arg4)
|
||||
}
|
||||
|
||||
// MockChangeBroadcaster is a mock of ChangeBroadcaster interface.
|
||||
type MockChangeBroadcaster struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockChangeBroadcasterMockRecorder
|
||||
}
|
||||
|
||||
// MockChangeBroadcasterMockRecorder is the mock recorder for MockChangeBroadcaster.
|
||||
type MockChangeBroadcasterMockRecorder struct {
|
||||
mock *MockChangeBroadcaster
|
||||
}
|
||||
|
||||
// NewMockChangeBroadcaster creates a new mock instance.
|
||||
func NewMockChangeBroadcaster(ctrl *gomock.Controller) *MockChangeBroadcaster {
|
||||
mock := &MockChangeBroadcaster{ctrl: ctrl}
|
||||
mock.recorder = &MockChangeBroadcasterMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockChangeBroadcaster) EXPECT() *MockChangeBroadcasterMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// BroadcastJobUpdate mocks base method.
|
||||
func (m *MockChangeBroadcaster) BroadcastJobUpdate(arg0 api.JobUpdate) {
|
||||
m.ctrl.T.Helper()
|
||||
m.ctrl.Call(m, "BroadcastJobUpdate", arg0)
|
||||
}
|
||||
|
||||
// BroadcastJobUpdate indicates an expected call of BroadcastJobUpdate.
|
||||
func (mr *MockChangeBroadcasterMockRecorder) BroadcastJobUpdate(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BroadcastJobUpdate", reflect.TypeOf((*MockChangeBroadcaster)(nil).BroadcastJobUpdate), arg0)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.blender.org/flamenco/internal/manager/persistence"
|
||||
"git.blender.org/flamenco/internal/manager/webupdates"
|
||||
"git.blender.org/flamenco/pkg/api"
|
||||
)
|
||||
|
||||
@ -19,11 +20,12 @@ const taskFailJobPercentage = 10 // Integer from 0 to 100.
|
||||
|
||||
// StateMachine handles task and job status changes.
|
||||
type StateMachine struct {
|
||||
persist PersistenceService
|
||||
persist PersistenceService
|
||||
broadcaster ChangeBroadcaster
|
||||
}
|
||||
|
||||
// Generate mock implementations of these interfaces.
|
||||
//go:generate go run github.com/golang/mock/mockgen -destination mocks/interfaces_mock.gen.go -package mocks git.blender.org/flamenco/internal/manager/task_state_machine PersistenceService
|
||||
//go:generate go run github.com/golang/mock/mockgen -destination mocks/interfaces_mock.gen.go -package mocks git.blender.org/flamenco/internal/manager/task_state_machine PersistenceService,ChangeBroadcaster
|
||||
|
||||
type PersistenceService interface {
|
||||
SaveTask(ctx context.Context, task *persistence.Task) error
|
||||
@ -45,9 +47,18 @@ type PersistenceService interface {
|
||||
// PersistenceService should be a subset of persistence.DB
|
||||
var _ PersistenceService = (*persistence.DB)(nil)
|
||||
|
||||
func NewStateMachine(persist PersistenceService) *StateMachine {
|
||||
type ChangeBroadcaster interface {
|
||||
// BroadcastJobUpdate sends the job update to clients.
|
||||
BroadcastJobUpdate(jobUpdate api.JobUpdate)
|
||||
}
|
||||
|
||||
// ChangeBroadcaster should be a subset of webupdates.BiDirComms
|
||||
var _ ChangeBroadcaster = (*webupdates.BiDirComms)(nil)
|
||||
|
||||
func NewStateMachine(persist PersistenceService, broadcaster ChangeBroadcaster) *StateMachine {
|
||||
return &StateMachine{
|
||||
persist: persist,
|
||||
persist: persist,
|
||||
broadcaster: broadcaster,
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,6 +255,15 @@ func (sm *StateMachine) JobStatusChange(ctx context.Context, job *persistence.Jo
|
||||
if err != nil {
|
||||
return fmt.Errorf("updating job's tasks after job status change: %w", err)
|
||||
}
|
||||
|
||||
// Broadcast this change to the SocketIO clients.
|
||||
jobUpdate := api.JobUpdate{
|
||||
Id: job.UUID,
|
||||
Updated: job.UpdatedAt,
|
||||
PreviousStatus: &oldJobStatus,
|
||||
Status: job.Status,
|
||||
}
|
||||
sm.broadcaster.BroadcastJobUpdate(jobUpdate)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -18,7 +18,8 @@ import (
|
||||
)
|
||||
|
||||
type StateMachineMocks struct {
|
||||
persist *mocks.MockPersistenceService
|
||||
persist *mocks.MockPersistenceService
|
||||
broadcaster *mocks.MockChangeBroadcaster
|
||||
}
|
||||
|
||||
// In the comments below, "T" indicates the performed task status change, and
|
||||
@ -32,6 +33,8 @@ func TestTaskStatusChangeQueuedToActive(t *testing.T) {
|
||||
task := taskWithStatus(api.JobStatusQueued, api.TaskStatusQueued)
|
||||
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusActive)
|
||||
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusActive)
|
||||
mocks.expectBroadcastJobChange(task.Job, api.JobStatusQueued, api.JobStatusActive)
|
||||
|
||||
assert.NoError(t, sm.TaskStatusChange(ctx, task, api.TaskStatusActive))
|
||||
}
|
||||
|
||||
@ -81,6 +84,8 @@ func TestTaskStatusChangeActiveToCompleted(t *testing.T) {
|
||||
mocks.expectSaveTaskWithStatus(t, task3, api.TaskStatusCompleted)
|
||||
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusCompleted).Return(3, 3, nil) // 3 of 3 complete.
|
||||
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusCompleted)
|
||||
mocks.expectBroadcastJobChange(task.Job, api.JobStatusActive, api.JobStatusCompleted)
|
||||
|
||||
assert.NoError(t, sm.TaskStatusChange(ctx, task3, api.TaskStatusCompleted))
|
||||
}
|
||||
|
||||
@ -93,6 +98,8 @@ func TestTaskStatusChangeQueuedToFailed(t *testing.T) {
|
||||
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusFailed)
|
||||
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusActive)
|
||||
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusFailed).Return(1, 100, nil) // 1 out of 100 failed.
|
||||
mocks.expectBroadcastJobChange(task.Job, api.JobStatusQueued, api.JobStatusActive)
|
||||
|
||||
assert.NoError(t, sm.TaskStatusChange(ctx, task, api.TaskStatusFailed))
|
||||
}
|
||||
|
||||
@ -104,6 +111,8 @@ func TestTaskStatusChangeActiveToFailedFailJob(t *testing.T) {
|
||||
task := taskWithStatus(api.JobStatusActive, api.TaskStatusActive)
|
||||
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusFailed)
|
||||
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusFailed)
|
||||
mocks.expectBroadcastJobChange(task.Job, api.JobStatusActive, api.JobStatusFailed)
|
||||
|
||||
mocks.persist.EXPECT().CountTasksOfJobInStatus(ctx, task.Job, api.TaskStatusFailed).Return(10, 100, nil) // 10 out of 100 failed.
|
||||
|
||||
// Expect failure of the job to trigger cancellation of remaining tasks.
|
||||
@ -127,6 +136,8 @@ func TestTaskStatusChangeRequeueOnCompletedJob(t *testing.T) {
|
||||
task := taskWithStatus(api.JobStatusCompleted, api.TaskStatusCompleted)
|
||||
mocks.expectSaveTaskWithStatus(t, task, api.TaskStatusQueued)
|
||||
mocks.expectSaveJobWithStatus(t, task.Job, api.JobStatusRequeued)
|
||||
mocks.expectBroadcastJobChange(task.Job, api.JobStatusCompleted, api.JobStatusRequeued)
|
||||
mocks.expectBroadcastJobChange(task.Job, api.JobStatusRequeued, api.JobStatusQueued)
|
||||
|
||||
// Expect queueing of the job to trigger queueing of all its tasks, if those tasks were all completed before.
|
||||
// 2 out of 3 completed, because one was just queued.
|
||||
@ -156,6 +167,8 @@ func TestTaskStatusChangeCancelSingleTask(t *testing.T) {
|
||||
mocks.expectSaveTaskWithStatus(t, task2, api.TaskStatusCanceled)
|
||||
mocks.persist.EXPECT().JobHasTasksInStatus(ctx, job, api.TaskStatusCancelRequested).Return(false, nil)
|
||||
mocks.expectSaveJobWithStatus(t, job, api.JobStatusCanceled)
|
||||
mocks.expectBroadcastJobChange(task.Job, api.JobStatusCancelRequested, api.JobStatusCanceled)
|
||||
|
||||
assert.NoError(t, sm.TaskStatusChange(ctx, task2, api.TaskStatusCanceled))
|
||||
}
|
||||
|
||||
@ -196,6 +209,10 @@ func TestJobRequeueWithSomeCompletedTasks(t *testing.T) {
|
||||
"Queued because job transitioned status from \"active\" to \"requeued\"",
|
||||
)
|
||||
mocks.expectSaveJobWithStatus(t, job, api.JobStatusQueued)
|
||||
|
||||
mocks.expectBroadcastJobChange(job, api.JobStatusActive, api.JobStatusRequeued)
|
||||
mocks.expectBroadcastJobChange(job, api.JobStatusRequeued, api.JobStatusQueued)
|
||||
|
||||
assert.NoError(t, sm.JobStatusChange(ctx, job, api.JobStatusRequeued))
|
||||
}
|
||||
|
||||
@ -224,14 +241,18 @@ func TestJobRequeueWithAllCompletedTasks(t *testing.T) {
|
||||
Return(0, 3, nil). // By now all tasks are queued.
|
||||
After(call3)
|
||||
|
||||
mocks.expectBroadcastJobChange(job, api.JobStatusCompleted, api.JobStatusRequeued)
|
||||
mocks.expectBroadcastJobChange(job, api.JobStatusRequeued, api.JobStatusQueued)
|
||||
|
||||
assert.NoError(t, sm.JobStatusChange(ctx, job, api.JobStatusRequeued))
|
||||
}
|
||||
|
||||
func mockedTaskStateMachine(mockCtrl *gomock.Controller) (*StateMachine, *StateMachineMocks) {
|
||||
mocks := StateMachineMocks{
|
||||
persist: mocks.NewMockPersistenceService(mockCtrl),
|
||||
persist: mocks.NewMockPersistenceService(mockCtrl),
|
||||
broadcaster: mocks.NewMockChangeBroadcaster(mockCtrl),
|
||||
}
|
||||
sm := NewStateMachine(mocks.persist)
|
||||
sm := NewStateMachine(mocks.persist, mocks.broadcaster)
|
||||
return sm, &mocks
|
||||
}
|
||||
|
||||
@ -261,6 +282,19 @@ func (m *StateMachineMocks) expectSaveJobWithStatus(
|
||||
})
|
||||
}
|
||||
|
||||
func (m *StateMachineMocks) expectBroadcastJobChange(
|
||||
job *persistence.Job,
|
||||
fromStatus, toStatus api.JobStatus,
|
||||
) *gomock.Call {
|
||||
expectUpdate := api.JobUpdate{
|
||||
Id: job.UUID,
|
||||
Updated: job.UpdatedAt,
|
||||
PreviousStatus: &fromStatus,
|
||||
Status: toStatus,
|
||||
}
|
||||
return m.broadcaster.EXPECT().BroadcastJobUpdate(expectUpdate)
|
||||
}
|
||||
|
||||
/* taskWithStatus() creates a task of a certain status, with a job of a certain status. */
|
||||
func taskWithStatus(jobStatus api.JobStatus, taskStatus api.TaskStatus) *persistence.Task {
|
||||
job := persistence.Job{
|
||||
|
19
internal/manager/webupdates/chatrooms.go
Normal file
19
internal/manager/webupdates/chatrooms.go
Normal file
@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package webupdates
|
||||
|
||||
type SocketIORoomName string
|
||||
|
||||
const (
|
||||
// Predefined SocketIO rooms.
|
||||
SocketIORoomChat SocketIORoomName = "Chat" // For chat messages.
|
||||
SocketIORoomJobs SocketIORoomName = "Jobs" // For job updates.
|
||||
)
|
||||
|
||||
type SocketIOEventType string
|
||||
|
||||
const (
|
||||
// Predefined SocketIO event types.
|
||||
SIOEventChatMessageRcv SocketIOEventType = "/chat" // clients send messages here
|
||||
SIOEventChatMessageSend SocketIOEventType = "/message" // messages are broadcasted here
|
||||
SIOEventJobUpdate SocketIOEventType = "/jobs" // sends api.JobUpdate
|
||||
)
|
25
internal/manager/webupdates/job_updates.go
Normal file
25
internal/manager/webupdates/job_updates.go
Normal file
@ -0,0 +1,25 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package webupdates
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.blender.org/flamenco/pkg/api"
|
||||
)
|
||||
|
||||
// BroadcastJobUpdate sends the job update to clients.
|
||||
func (b *BiDirComms) BroadcastJobUpdate(jobUpdate api.JobUpdate) {
|
||||
log.Debug().Interface("jobUpdate", jobUpdate).Msg("socketIO: broadcasting job update")
|
||||
b.sockserv.BroadcastTo(string(SocketIORoomJobs), "/jobs", jobUpdate)
|
||||
}
|
||||
|
||||
// BroadcastNewJob sends a "new job" notification to clients.
|
||||
func (b *BiDirComms) BroadcastNewJob(jobUpdate api.JobUpdate) {
|
||||
if jobUpdate.PreviousStatus != nil {
|
||||
log.Warn().Interface("jobUpdate", jobUpdate).Msg("socketIO: new jobs should not have a previous state")
|
||||
jobUpdate.PreviousStatus = nil
|
||||
}
|
||||
|
||||
log.Debug().Interface("jobUpdate", jobUpdate).Msg("socketIO: broadcasting new job")
|
||||
b.sockserv.BroadcastTo(string(SocketIORoomJobs), "/jobs", jobUpdate)
|
||||
}
|
@ -37,7 +37,7 @@ func socketIOServer() *gosocketio.Server {
|
||||
// socket connection
|
||||
err = sio.On(gosocketio.OnConnection, func(c *gosocketio.Channel) {
|
||||
log.Debug().Str("clientID", c.Id()).Msg("socketIO: connected")
|
||||
if err := c.Join("Room"); err != nil {
|
||||
if err := c.Join(string(SocketIORoomChat)); err != nil {
|
||||
log.Warn().Err(err).Str("clientID", c.Id()).Msg("socketIO: unable to make client join broadcast message room")
|
||||
}
|
||||
})
|
||||
@ -48,7 +48,7 @@ func socketIOServer() *gosocketio.Server {
|
||||
// socket disconnection
|
||||
err = sio.On(gosocketio.OnDisconnection, func(c *gosocketio.Channel) {
|
||||
log.Debug().Str("clientID", c.Id()).Msg("socketIO: disconnected")
|
||||
if err := c.Leave("Room"); err != nil {
|
||||
if err := c.Leave(string(SocketIORoomChat)); err != nil {
|
||||
log.Warn().Err(err).Str("clientID", c.Id()).Msg("socketIO: unable to make client leave broadcast message room")
|
||||
}
|
||||
})
|
||||
@ -64,12 +64,12 @@ func socketIOServer() *gosocketio.Server {
|
||||
}
|
||||
|
||||
// chat socket
|
||||
err = sio.On("/chat", func(c *gosocketio.Channel, message Message) string {
|
||||
err = sio.On(string(SIOEventChatMessageRcv), func(c *gosocketio.Channel, message Message) string {
|
||||
log.Info().Str("clientID", c.Id()).
|
||||
Str("text", message.Text).
|
||||
Str("name", message.Name).
|
||||
Msg("socketIO: message received")
|
||||
c.BroadcastTo("Room", "/message", message.Text)
|
||||
c.BroadcastTo(string(SocketIORoomChat), string(SIOEventChatMessageSend), message)
|
||||
return "message sent successfully."
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -892,6 +892,25 @@ components:
|
||||
"status": {$ref: "#/components/schemas/ShamanFileStatus"}
|
||||
required: [status]
|
||||
|
||||
# SocketIO API. These types are not used in any HTTP operation defined in
|
||||
# the 'paths' section of this document, so some code generators may choose
|
||||
# to skip these.
|
||||
|
||||
JobUpdate:
|
||||
type: object
|
||||
properties:
|
||||
"id":
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUID of the Job
|
||||
"updated":
|
||||
type: string
|
||||
format: date-time
|
||||
description: Timestamp of last update
|
||||
"status": {$ref: "#/components/schemas/JobStatus"}
|
||||
"previous_status": {$ref: "#/components/schemas/JobStatus"}
|
||||
required: [id, updated, status]
|
||||
|
||||
securitySchemes:
|
||||
worker_auth:
|
||||
description: Username is the worker ID, password is the secret given at worker registration.
|
||||
|
@ -1,8 +1,7 @@
|
||||
//go:generate oapi-codegen -generate types -o openapi_types.gen.go -package api flamenco-manager.yaml
|
||||
//go:generate oapi-codegen -generate server -o openapi_server.gen.go -package api flamenco-manager.yaml
|
||||
//go:generate oapi-codegen -generate spec -o openapi_spec.gen.go -package api flamenco-manager.yaml
|
||||
//go:generate oapi-codegen -generate client -o openapi_client.gen.go -package api flamenco-manager.yaml
|
||||
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
package api
|
||||
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//go:generate oapi-codegen -generate types,skip-prune -o openapi_types.gen.go -package api flamenco-manager.yaml
|
||||
//go:generate oapi-codegen -generate server,skip-prune -o openapi_server.gen.go -package api flamenco-manager.yaml
|
||||
//go:generate oapi-codegen -generate spec,skip-prune -o openapi_spec.gen.go -package api flamenco-manager.yaml
|
||||
//go:generate oapi-codegen -generate client,skip-prune -o openapi_client.gen.go -package api flamenco-manager.yaml
|
||||
|
@ -18,100 +18,101 @@ import (
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"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",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification file
|
||||
|
@ -225,6 +225,17 @@ type JobSettings struct {
|
||||
// JobStatus defines model for JobStatus.
|
||||
type JobStatus string
|
||||
|
||||
// JobUpdate defines model for JobUpdate.
|
||||
type JobUpdate struct {
|
||||
// UUID of the Job
|
||||
Id string `json:"id"`
|
||||
PreviousStatus *JobStatus `json:"previous_status,omitempty"`
|
||||
Status JobStatus `json:"status"`
|
||||
|
||||
// Timestamp of last update
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
// JobsQuery defines model for JobsQuery.
|
||||
type JobsQuery struct {
|
||||
Limit *int `json:"limit,omitempty"`
|
||||
|
@ -5,46 +5,45 @@
|
||||
<chat-chatbox
|
||||
@sendMessage="sendMessage"
|
||||
:chatHistory="messages"
|
||||
></chat-chatbox>
|
||||
/>
|
||||
<jobs-listener
|
||||
ref="jobsListener"
|
||||
:websocketURL="websocketURL"
|
||||
@jobUpdate="onJobUpdate"
|
||||
@message="onChatMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import io from "socket.io-client";
|
||||
import ChatNavbar from "./components/ChatNavbar.vue";
|
||||
import ChatChatbox from "./components/ChatChatbox.vue";
|
||||
import JobsListener from "./components/JobsListener.vue";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
name: "FlamencoWebApp",
|
||||
components: {
|
||||
ChatNavbar,
|
||||
ChatChatbox,
|
||||
JobsListener,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
socket: null,
|
||||
serverUrl: process.env.VUE_APP_SOCKET_URL || "ws://localhost:8080",
|
||||
websocketURL: "ws://localhost:8080",
|
||||
messages: [],
|
||||
};
|
||||
},
|
||||
mounted: function () {
|
||||
this.connectToWebsocket();
|
||||
},
|
||||
methods: {
|
||||
connectToWebsocket() {
|
||||
console.log("connecting to WS", this.serverUrl);
|
||||
this.socket = io(this.serverUrl, {
|
||||
transports: ["websocket"],
|
||||
});
|
||||
this.socket.on("/message", (message) => {
|
||||
console.log("message received: ", message);
|
||||
this.messages.push(message);
|
||||
});
|
||||
},
|
||||
sendMessage(message) {
|
||||
const payload = { name: "Nikita", text: message };
|
||||
console.log("sending:", payload);
|
||||
this.socket.emit("/chat", payload);
|
||||
this.$refs.jobsListener.sendBroadcastMessage("typer", message);
|
||||
},
|
||||
onJobUpdate(jobUpdate) {
|
||||
console.log("job update received:", jobUpdate);
|
||||
},
|
||||
onChatMessage(message) {
|
||||
console.log("chat message received:", message);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
63
web/app/src/components/JobsListener.vue
Normal file
63
web/app/src/components/JobsListener.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<span />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import io from "socket.io-client";
|
||||
|
||||
export default {
|
||||
emits: ["jobUpdate", "taskUpdate", "message"],
|
||||
props: ["websocketURL"],
|
||||
data() {
|
||||
return {
|
||||
socket: null,
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.connectToWebsocket();
|
||||
},
|
||||
beforeDestroy: function() {
|
||||
this.disconnectWebsocket();
|
||||
},
|
||||
methods: {
|
||||
connectToWebsocket() {
|
||||
// The SocketIO client API docs are available at:
|
||||
// https://github.com/socketio/socket.io-client/blob/2.4.x/docs/API.md
|
||||
console.log("connecting JobsListener to WS", this.websocketURL);
|
||||
this.socket = io(this.websocketURL, {
|
||||
transports: ["websocket"],
|
||||
});
|
||||
|
||||
this.socket.on("reconnect", (attemptNumber) => {
|
||||
console.log("socketIO reconnected after", attemptNumber, "attempts");
|
||||
})
|
||||
|
||||
this.socket.on("/jobs", (jobUpdate) => {
|
||||
this.$emit("jobUpdate", jobUpdate);
|
||||
});
|
||||
|
||||
// Chat system, useful for debugging.
|
||||
this.socket.on("/message", (message) => {
|
||||
this.$emit("message", message);
|
||||
});
|
||||
},
|
||||
|
||||
disconnectWebsocket() {
|
||||
if (this.socket == null) {
|
||||
console.log("no JobListener socket to disconnect");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("disconnecting JobsListener WS", this.websocketURL);
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
},
|
||||
|
||||
sendBroadcastMessage(name, message) {
|
||||
const payload = { name: name, text: message };
|
||||
console.log("sending broadcast message:", payload);
|
||||
this.socket.emit("/chat", payload);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -10,34 +10,27 @@ import {
|
||||
BButton,
|
||||
} from "bootstrap-vue";
|
||||
|
||||
import URLs from './urls'
|
||||
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css";
|
||||
|
||||
let url = new URL(window.location);
|
||||
url.port = "8080";
|
||||
const flamencoAPIURL = url.href;
|
||||
url.protocol = "ws:";
|
||||
const websocketURL = url.href;
|
||||
console.log("Flamenco API:", flamencoAPIURL);
|
||||
console.log("Websocket :", websocketURL);
|
||||
// let flamencoManager = require('flamenco-manager');
|
||||
// let apiClient = new flamencoManager.ApiClient(URLs.api);
|
||||
|
||||
let flamencoManager = require('flamenco-manager');
|
||||
let apiClient = new flamencoManager.ApiClient(flamencoAPIURL);
|
||||
// let query = new flamencoManager.JobsQuery();
|
||||
// // query.status_in = ["active"];
|
||||
// query.metadata = {project: "Heist"};
|
||||
|
||||
let query = new flamencoManager.JobsQuery();
|
||||
// query.status_in = ["active"];
|
||||
query.metadata = {project: "Heist"};
|
||||
|
||||
let JobsApi = new flamencoManager.JobsApi(apiClient);
|
||||
JobsApi.queryJobs(query).then(function(data) {
|
||||
console.log('API called successfully.');
|
||||
console.log(data);
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
});
|
||||
// let JobsApi = new flamencoManager.JobsApi(apiClient);
|
||||
// JobsApi.queryJobs(query).then(function(data) {
|
||||
// console.log('API called successfully.');
|
||||
// console.log(data);
|
||||
// }, function(error) {
|
||||
// console.error(error);
|
||||
// });
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.config.serverUrl = websocketURL;
|
||||
|
||||
Vue.use(FormInputPlugin);
|
||||
Vue.use(NavbarPlugin);
|
||||
@ -47,8 +40,7 @@ Vue.component("b-input-group", BInputGroup);
|
||||
Vue.component("b-button", BButton);
|
||||
Vue.use(IconsPlugin);
|
||||
|
||||
var vueApp = new Vue({
|
||||
render: h => h(App),
|
||||
});
|
||||
var vueApp = new Vue(App);
|
||||
vueApp.websocketURL = URLs.ws;
|
||||
|
||||
vueApp.$mount("#app");
|
||||
|
16
web/app/src/urls.js
Normal file
16
web/app/src/urls.js
Normal file
@ -0,0 +1,16 @@
|
||||
let url = new URL(window.location);
|
||||
url.port = "8080";
|
||||
const flamencoAPIURL = url.href;
|
||||
|
||||
url.protocol = "ws:";
|
||||
const websocketURL = url.href;
|
||||
|
||||
const URLs = {
|
||||
api: flamencoAPIURL,
|
||||
ws: websocketURL,
|
||||
};
|
||||
|
||||
console.log("Flamenco API:", URLs.api);
|
||||
console.log("Websocket :", URLs.ws);
|
||||
|
||||
export default URLs;
|
@ -152,6 +152,7 @@ Class | Method | HTTP request | Description
|
||||
- [flamencoManager.Job](docs/Job.md)
|
||||
- [flamencoManager.JobAllOf](docs/JobAllOf.md)
|
||||
- [flamencoManager.JobStatus](docs/JobStatus.md)
|
||||
- [flamencoManager.JobUpdate](docs/JobUpdate.md)
|
||||
- [flamencoManager.JobsQuery](docs/JobsQuery.md)
|
||||
- [flamencoManager.JobsQueryResult](docs/JobsQueryResult.md)
|
||||
- [flamencoManager.ManagerConfiguration](docs/ManagerConfiguration.md)
|
||||
|
12
web/manager-api/docs/JobUpdate.md
Normal file
12
web/manager-api/docs/JobUpdate.md
Normal file
@ -0,0 +1,12 @@
|
||||
# flamencoManager.JobUpdate
|
||||
|
||||
## Properties
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**id** | **String** | UUID of the Job |
|
||||
**updated** | **Date** | Timestamp of last update |
|
||||
**status** | [**JobStatus**](JobStatus.md) | |
|
||||
**previous_status** | [**JobStatus**](JobStatus.md) | | [optional]
|
||||
|
||||
|
@ -55,7 +55,7 @@ class ApiClient {
|
||||
* @default {}
|
||||
*/
|
||||
this.defaultHeaders = {
|
||||
'User-Agent': 'Flamenco/c00cf8b0-dirty / webbrowser'
|
||||
'User-Agent': 'Flamenco/c875745b-dirty / webbrowser'
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -25,6 +25,7 @@ import FlamencoVersion from './model/FlamencoVersion';
|
||||
import Job from './model/Job';
|
||||
import JobAllOf from './model/JobAllOf';
|
||||
import JobStatus from './model/JobStatus';
|
||||
import JobUpdate from './model/JobUpdate';
|
||||
import JobsQuery from './model/JobsQuery';
|
||||
import JobsQueryResult from './model/JobsQueryResult';
|
||||
import ManagerConfiguration from './model/ManagerConfiguration';
|
||||
@ -162,6 +163,12 @@ export {
|
||||
*/
|
||||
JobStatus,
|
||||
|
||||
/**
|
||||
* The JobUpdate model constructor.
|
||||
* @property {module:model/JobUpdate}
|
||||
*/
|
||||
JobUpdate,
|
||||
|
||||
/**
|
||||
* The JobsQuery model constructor.
|
||||
* @property {module:model/JobsQuery}
|
||||
|
104
web/manager-api/src/model/JobUpdate.js
Normal file
104
web/manager-api/src/model/JobUpdate.js
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Flamenco manager
|
||||
* Render Farm manager API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*
|
||||
*/
|
||||
|
||||
import ApiClient from '../ApiClient';
|
||||
import JobStatus from './JobStatus';
|
||||
|
||||
/**
|
||||
* The JobUpdate model module.
|
||||
* @module model/JobUpdate
|
||||
* @version 0.0.0
|
||||
*/
|
||||
class JobUpdate {
|
||||
/**
|
||||
* Constructs a new <code>JobUpdate</code>.
|
||||
* @alias module:model/JobUpdate
|
||||
* @param id {String} UUID of the Job
|
||||
* @param updated {Date} Timestamp of last update
|
||||
* @param status {module:model/JobStatus}
|
||||
*/
|
||||
constructor(id, updated, status) {
|
||||
|
||||
JobUpdate.initialize(this, id, updated, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the fields of this object.
|
||||
* This method is used by the constructors of any subclasses, in order to implement multiple inheritance (mix-ins).
|
||||
* Only for internal use.
|
||||
*/
|
||||
static initialize(obj, id, updated, status) {
|
||||
obj['id'] = id;
|
||||
obj['updated'] = updated;
|
||||
obj['status'] = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a <code>JobUpdate</code> from a plain JavaScript object, optionally creating a new instance.
|
||||
* Copies all relevant properties from <code>data</code> to <code>obj</code> if supplied or a new instance if not.
|
||||
* @param {Object} data The plain JavaScript object bearing properties of interest.
|
||||
* @param {module:model/JobUpdate} obj Optional instance to populate.
|
||||
* @return {module:model/JobUpdate} The populated <code>JobUpdate</code> instance.
|
||||
*/
|
||||
static constructFromObject(data, obj) {
|
||||
if (data) {
|
||||
obj = obj || new JobUpdate();
|
||||
|
||||
if (data.hasOwnProperty('id')) {
|
||||
obj['id'] = ApiClient.convertToType(data['id'], 'String');
|
||||
}
|
||||
if (data.hasOwnProperty('updated')) {
|
||||
obj['updated'] = ApiClient.convertToType(data['updated'], 'Date');
|
||||
}
|
||||
if (data.hasOwnProperty('status')) {
|
||||
obj['status'] = JobStatus.constructFromObject(data['status']);
|
||||
}
|
||||
if (data.hasOwnProperty('previous_status')) {
|
||||
obj['previous_status'] = JobStatus.constructFromObject(data['previous_status']);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID of the Job
|
||||
* @member {String} id
|
||||
*/
|
||||
JobUpdate.prototype['id'] = undefined;
|
||||
|
||||
/**
|
||||
* Timestamp of last update
|
||||
* @member {Date} updated
|
||||
*/
|
||||
JobUpdate.prototype['updated'] = undefined;
|
||||
|
||||
/**
|
||||
* @member {module:model/JobStatus} status
|
||||
*/
|
||||
JobUpdate.prototype['status'] = undefined;
|
||||
|
||||
/**
|
||||
* @member {module:model/JobStatus} previous_status
|
||||
*/
|
||||
JobUpdate.prototype['previous_status'] = undefined;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export default JobUpdate;
|
||||
|
Loading…
x
Reference in New Issue
Block a user