Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
dc3d354a
Commit
dc3d354a
authored
Apr 07, 2021
by
Frédéric Caplette
Committed by
Sarah Groff Hennigh-Palermo
Apr 07, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Resolve "Apply Links Layer to Pipeline Editing Vis"
parent
2927d330
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
142 additions
and
125 deletions
+142
-125
app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
...ascripts/pipelines/components/pipeline_graph/job_pill.vue
+8
-1
app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
...ts/pipelines/components/pipeline_graph/pipeline_graph.vue
+78
-116
spec/frontend/pipelines/pipeline_graph/mock_data.js
spec/frontend/pipelines/pipeline_graph/mock_data.js
+36
-0
spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
.../frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+20
-8
No files found.
app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
View file @
dc3d354a
...
@@ -10,6 +10,10 @@ export default {
...
@@ -10,6 +10,10 @@ export default {
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
pipelineId
:
{
type
:
Number
,
required
:
true
,
},
isHighlighted
:
{
isHighlighted
:
{
type
:
Boolean
,
type
:
Boolean
,
required
:
false
,
required
:
false
,
...
@@ -32,6 +36,9 @@ export default {
...
@@ -32,6 +36,9 @@ export default {
},
},
},
},
computed
:
{
computed
:
{
id
()
{
return
`
${
this
.
jobName
}
-
${
this
.
pipelineId
}
`
;
},
jobPillClasses
()
{
jobPillClasses
()
{
return
[
return
[
{
'
gl-opacity-3
'
:
this
.
isFadedOut
},
{
'
gl-opacity-3
'
:
this
.
isFadedOut
},
...
@@ -52,7 +59,7 @@ export default {
...
@@ -52,7 +59,7 @@ export default {
<
template
>
<
template
>
<tooltip-on-truncate
:title=
"jobName"
truncate-target=
"child"
placement=
"top"
>
<tooltip-on-truncate
:title=
"jobName"
truncate-target=
"child"
placement=
"top"
>
<div
<div
:id=
"
jobName
"
:id=
"
id
"
class=
"gl-w-15 gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
class=
"gl-w-15 gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
:class=
"jobPillClasses"
:class=
"jobPillClasses"
@
mouseover=
"onMouseEnter"
@
mouseover=
"onMouseEnter"
...
...
app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
View file @
dc3d354a
...
@@ -3,9 +3,7 @@ import { GlAlert } from '@gitlab/ui';
...
@@ -3,9 +3,7 @@ import { GlAlert } from '@gitlab/ui';
import
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
CI_CONFIG_STATUS_INVALID
}
from
'
~/pipeline_editor/constants
'
;
import
{
CI_CONFIG_STATUS_INVALID
}
from
'
~/pipeline_editor/constants
'
;
import
{
DRAW_FAILURE
,
DEFAULT
,
INVALID_CI_CONFIG
,
EMPTY_PIPELINE_DATA
}
from
'
../../constants
'
;
import
{
DRAW_FAILURE
,
DEFAULT
,
INVALID_CI_CONFIG
,
EMPTY_PIPELINE_DATA
}
from
'
../../constants
'
;
import
{
createJobsHash
,
generateJobNeedsDict
}
from
'
../../utils
'
;
import
LinksLayer
from
'
../graph_shared/links_layer.vue
'
;
import
{
generateLinksData
}
from
'
../graph_shared/drawing_utils
'
;
import
{
parseData
}
from
'
../parsing_utils
'
;
import
JobPill
from
'
./job_pill.vue
'
;
import
JobPill
from
'
./job_pill.vue
'
;
import
StagePill
from
'
./stage_pill.vue
'
;
import
StagePill
from
'
./stage_pill.vue
'
;
...
@@ -13,10 +11,12 @@ export default {
...
@@ -13,10 +11,12 @@ export default {
components
:
{
components
:
{
GlAlert
,
GlAlert
,
JobPill
,
JobPill
,
LinksLayer
,
StagePill
,
StagePill
,
},
},
CONTAINER_REF
:
'
PIPELINE_GRAPH_CONTAINER_REF
'
,
CONTAINER_REF
:
'
PIPELINE_GRAPH_CONTAINER_REF
'
,
CONTAINER_ID
:
'
pipeline-graph-container
'
,
BASE_CONTAINER_ID
:
'
pipeline-graph-container
'
,
PIPELINE_ID
:
0
,
STROKE_WIDTH
:
2
,
STROKE_WIDTH
:
2
,
errorTexts
:
{
errorTexts
:
{
[
DRAW_FAILURE
]:
__
(
'
Could not draw the lines for job relationships
'
),
[
DRAW_FAILURE
]:
__
(
'
Could not draw the lines for job relationships
'
),
...
@@ -36,33 +36,16 @@ export default {
...
@@ -36,33 +36,16 @@ export default {
return
{
return
{
failureType
:
null
,
failureType
:
null
,
highlightedJob
:
null
,
highlightedJob
:
null
,
links
:
[],
highlightedJobs
:
[],
needsObject
:
null
,
measurements
:
{
height
:
0
,
height
:
0
,
width
:
0
,
width
:
0
,
},
};
};
},
},
computed
:
{
computed
:
{
hideGraph
()
{
containerId
()
{
// We won't even try to render the graph with these condition
return
`
${
this
.
$options
.
BASE_CONTAINER_ID
}
-
${
this
.
$options
.
PIPELINE_ID
}
`
;
// because it would cause additional errors down the line for the user
// which is confusing.
return
this
.
isPipelineDataEmpty
||
this
.
isInvalidCiConfig
;
},
pipelineStages
()
{
return
this
.
pipelineData
?.
stages
||
[];
},
isPipelineDataEmpty
()
{
return
!
this
.
isInvalidCiConfig
&&
this
.
pipelineStages
.
length
===
0
;
},
isInvalidCiConfig
()
{
return
this
.
pipelineData
?.
status
===
CI_CONFIG_STATUS_INVALID
;
},
hasError
()
{
return
this
.
failureType
;
},
hasHighlightedJob
()
{
return
Boolean
(
this
.
highlightedJob
);
},
},
failure
()
{
failure
()
{
switch
(
this
.
failureType
)
{
switch
(
this
.
failureType
)
{
...
@@ -92,28 +75,26 @@ export default {
...
@@ -92,28 +75,26 @@ export default {
};
};
}
}
},
},
viewBox
()
{
hasError
()
{
return
[
0
,
0
,
this
.
width
,
this
.
height
]
;
return
this
.
failureType
;
},
},
highlightedJobs
()
{
hasHighlightedJob
()
{
// If you are hovering on a job, then the jobs we want to highlight are:
return
Boolean
(
this
.
highlightedJob
);
// The job you are currently hovering + all of its needs.
return
[
this
.
highlightedJob
,
...
this
.
needsObject
[
this
.
highlightedJob
]];
},
},
hi
ghlightedLinks
()
{
hi
deGraph
()
{
//
If you are hovering on a job, then the links we want to highlight are:
//
We won't even try to render the graph with these condition
//
All the links whose `source` and `target` are highlighted jobs.
//
because it would cause additional errors down the line for the user
if
(
this
.
hasHighlightedJob
)
{
// which is confusing.
const
filteredLinks
=
this
.
links
.
filter
((
link
)
=>
{
return
this
.
isPipelineDataEmpty
||
this
.
isInvalidCiConfig
;
return
(
},
this
.
highlightedJobs
.
includes
(
link
.
source
)
&&
this
.
highlightedJobs
.
includes
(
link
.
target
)
isInvalidCiConfig
()
{
)
;
return
this
.
pipelineData
?.
status
===
CI_CONFIG_STATUS_INVALID
;
});
},
isPipelineDataEmpty
()
{
return
filteredLinks
.
map
((
link
)
=>
link
.
ref
)
;
return
!
this
.
isInvalidCiConfig
&&
this
.
pipelineStages
.
length
===
0
;
}
},
pipelineStages
()
{
return
[];
return
this
.
pipelineData
?.
stages
||
[];
},
},
},
},
watch
:
{
watch
:
{
...
@@ -127,21 +108,17 @@ export default {
...
@@ -127,21 +108,17 @@ export default {
}
else
{
}
else
{
this
.
$nextTick
(()
=>
{
this
.
$nextTick
(()
=>
{
this
.
computeGraphDimensions
();
this
.
computeGraphDimensions
();
this
.
prepareLinkData
();
});
});
}
}
},
},
},
},
},
},
methods
:
{
methods
:
{
prepareLinkData
()
{
computeGraphDimensions
()
{
try
{
this
.
measurements
=
{
const
arrayOfJobs
=
this
.
pipelineStages
.
flatMap
(({
groups
})
=>
groups
);
width
:
this
.
$refs
[
this
.
$options
.
CONTAINER_REF
].
scrollWidth
,
const
parsedData
=
parseData
(
arrayOfJobs
);
height
:
this
.
$refs
[
this
.
$options
.
CONTAINER_REF
].
scrollHeight
,
this
.
links
=
generateLinksData
(
parsedData
,
this
.
$options
.
CONTAINER_ID
);
};
}
catch
{
this
.
reportFailure
(
DRAW_FAILURE
);
}
},
},
getStageBackgroundClasses
(
index
)
{
getStageBackgroundClasses
(
index
)
{
const
{
length
}
=
this
.
pipelineStages
;
const
{
length
}
=
this
.
pipelineStages
;
...
@@ -161,22 +138,14 @@ export default {
...
@@ -161,22 +138,14 @@ export default {
return
''
;
return
''
;
},
},
highlightNeeds
(
uniqueJobId
)
{
isJobHighlighted
(
jobName
)
{
// The first time we hover, we create the object where
return
this
.
highlightedJobs
.
includes
(
jobName
);
// we store all the data to properly highlight the needs.
if
(
!
this
.
needsObject
)
{
const
jobs
=
createJobsHash
(
this
.
pipelineStages
);
this
.
needsObject
=
generateJobNeedsDict
(
jobs
)
??
{};
}
this
.
highlightedJob
=
uniqueJobId
;
},
},
removeHighlightNeeds
(
)
{
onError
(
error
)
{
this
.
highlightedJob
=
null
;
this
.
reportFailure
(
error
.
type
)
;
},
},
computeGraphDimensions
()
{
removeHoveredJob
()
{
this
.
width
=
`
${
this
.
$refs
[
this
.
$options
.
CONTAINER_REF
].
scrollWidth
}
`
;
this
.
highlightedJob
=
null
;
this
.
height
=
`
${
this
.
$refs
[
this
.
$options
.
CONTAINER_REF
].
scrollHeight
}
`
;
},
},
reportFailure
(
errorType
)
{
reportFailure
(
errorType
)
{
this
.
failureType
=
errorType
;
this
.
failureType
=
errorType
;
...
@@ -184,17 +153,11 @@ export default {
...
@@ -184,17 +153,11 @@ export default {
resetFailure
()
{
resetFailure
()
{
this
.
failureType
=
null
;
this
.
failureType
=
null
;
},
},
isJobHighlighted
(
jobName
)
{
setHoveredJob
(
jobName
)
{
return
this
.
highlightedJobs
.
includes
(
jobName
);
this
.
highlightedJob
=
jobName
;
},
isLinkHighlighted
(
linkRef
)
{
return
this
.
highlightedLinks
.
includes
(
linkRef
);
},
},
getLinkClasses
(
link
)
{
updateHighlightedJobs
(
jobs
)
{
return
[
this
.
highlightedJobs
=
jobs
;
this
.
isLinkHighlighted
(
link
.
ref
)
?
'
gl-stroke-blue-400
'
:
'
gl-stroke-gray-200
'
,
{
'
gl-opacity-3
'
:
this
.
hasHighlightedJob
&&
!
this
.
isLinkHighlighted
(
link
.
ref
)
},
];
},
},
},
},
};
};
...
@@ -211,48 +174,47 @@ export default {
...
@@ -211,48 +174,47 @@ export default {
</gl-alert>
</gl-alert>
<div
<div
v-if=
"!hideGraph"
v-if=
"!hideGraph"
:id=
"
$options.CONTAINER_ID
"
:id=
"
containerId
"
:ref=
"$options.CONTAINER_REF"
:ref=
"$options.CONTAINER_REF"
class=
"gl-display-flex gl-bg-gray-50 gl-px-4 gl-overflow-auto gl-relative gl-py-7"
data-testid=
"graph-container"
data-testid=
"graph-container"
>
>
<svg
:viewBox=
"viewBox"
:width=
"width"
:height=
"height"
class=
"gl-absolute"
>
<links-layer
<path
:pipeline-data=
"pipelineStages"
v-for=
"link in links"
:pipeline-id=
"$options.PIPELINE_ID"
:key=
"link.path"
:container-id=
"containerId"
:ref=
"link.ref"
:container-measurements=
"measurements"
:d=
"link.path"
:highlighted-job=
"highlightedJob"
class=
"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
@
highlightedJobsChange=
"updateHighlightedJobs"
:class=
"getLinkClasses(link)"
@
error=
"onError"
:stroke-width=
"$options.STROKE_WIDTH"
/>
</svg>
<div
v-for=
"(stage, index) in pipelineStages"
:key=
"`$
{stage.name}-${index}`"
class="gl-flex-direction-column"
>
>
<div
<div
class=
"gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
v-for=
"(stage, index) in pipelineStages"
:class=
"getStageBackgroundClasses(index)"
:key=
"`$
{stage.name}-${index}`"
data-testid=
"stage-background"
class="gl-flex-direction-column"
>
<stage-pill
:stage-name=
"stage.name"
:is-empty=
"stage.groups.length === 0"
/>
</div>
<div
class=
"gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
>
>
<job-pill
<div
v-for=
"group in stage.groups"
class=
"gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
:key=
"group.name"
:class=
"getStageBackgroundClasses(index)"
:job-name=
"group.name"
data-testid=
"stage-background"
:is-highlighted=
"hasHighlightedJob && isJobHighlighted(group.name)"
>
:is-faded-out=
"hasHighlightedJob && !isJobHighlighted(group.name)"
<stage-pill
:stage-name=
"stage.name"
:is-empty=
"stage.groups.length === 0"
/>
@
on-mouse-enter=
"highlightNeeds"
</div>
@
on-mouse-leave=
"removeHighlightNeeds"
<div
/>
class=
"gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
>
<job-pill
v-for=
"group in stage.groups"
:key=
"group.name"
:job-name=
"group.name"
:pipeline-id=
"$options.PIPELINE_ID"
:is-highlighted=
"hasHighlightedJob && isJobHighlighted(group.name)"
:is-faded-out=
"hasHighlightedJob && !isJobHighlighted(group.name)"
@
on-mouse-enter=
"setHoveredJob"
@
on-mouse-leave=
"removeHoveredJob"
/>
</div>
</div>
</div>
</
div
>
</
links-layer
>
</div>
</div>
</div>
</div>
</
template
>
</
template
>
spec/frontend/pipelines/pipeline_graph/mock_data.js
View file @
dc3d354a
...
@@ -98,6 +98,42 @@ export const pipelineData = {
...
@@ -98,6 +98,42 @@ export const pipelineData = {
],
],
};
};
export
const
invalidNeedsData
=
{
stages
:
[
{
name
:
'
build
'
,
groups
:
[
{
name
:
'
build_1
'
,
jobs
:
[{
script
:
'
echo hello
'
,
stage
:
'
build
'
}],
},
],
},
{
name
:
'
test
'
,
groups
:
[
{
name
:
'
test_1
'
,
jobs
:
[{
script
:
'
yarn test
'
,
stage
:
'
test
'
}],
},
{
name
:
'
test_2
'
,
jobs
:
[{
script
:
'
yarn karma
'
,
stage
:
'
test
'
}],
},
],
},
{
name
:
'
deploy
'
,
groups
:
[
{
name
:
'
deploy_1
'
,
jobs
:
[{
script
:
'
yarn magick
'
,
stage
:
'
deploy
'
,
needs
:
[
'
invalid_job
'
]
}],
},
],
},
],
};
export
const
parallelNeedData
=
{
export
const
parallelNeedData
=
{
stages
:
[
stages
:
[
{
{
...
...
spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
View file @
dc3d354a
import
{
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
CI_CONFIG_STATUS_INVALID
,
CI_CONFIG_STATUS_VALID
}
from
'
~/pipeline_editor/constants
'
;
import
{
CI_CONFIG_STATUS_INVALID
,
CI_CONFIG_STATUS_VALID
}
from
'
~/pipeline_editor/constants
'
;
import
LinksInner
from
'
~/pipelines/components/graph_shared/links_inner.vue
'
;
import
LinksLayer
from
'
~/pipelines/components/graph_shared/links_layer.vue
'
;
import
JobPill
from
'
~/pipelines/components/pipeline_graph/job_pill.vue
'
;
import
JobPill
from
'
~/pipelines/components/pipeline_graph/job_pill.vue
'
;
import
PipelineGraph
from
'
~/pipelines/components/pipeline_graph/pipeline_graph.vue
'
;
import
PipelineGraph
from
'
~/pipelines/components/pipeline_graph/pipeline_graph.vue
'
;
import
StagePill
from
'
~/pipelines/components/pipeline_graph/stage_pill.vue
'
;
import
StagePill
from
'
~/pipelines/components/pipeline_graph/stage_pill.vue
'
;
import
{
DRAW_FAILURE
,
EMPTY_PIPELINE_DATA
,
INVALID_CI_CONFIG
}
from
'
~/pipelines/constants
'
;
import
{
DRAW_FAILURE
,
EMPTY_PIPELINE_DATA
,
INVALID_CI_CONFIG
}
from
'
~/pipelines/constants
'
;
import
{
pipelineData
,
singleStageData
}
from
'
./mock_data
'
;
import
{
invalidNeedsData
,
pipelineData
,
singleStageData
}
from
'
./mock_data
'
;
describe
(
'
pipeline graph component
'
,
()
=>
{
describe
(
'
pipeline graph component
'
,
()
=>
{
const
defaultProps
=
{
pipelineData
};
const
defaultProps
=
{
pipelineData
};
...
@@ -16,19 +18,28 @@ describe('pipeline graph component', () => {
...
@@ -16,19 +18,28 @@ describe('pipeline graph component', () => {
propsData
:
{
propsData
:
{
...
props
,
...
props
,
},
},
stubs
:
{
LinksLayer
,
LinksInner
},
data
()
{
return
{
measurements
:
{
width
:
1000
,
height
:
1000
,
},
};
},
});
});
};
};
const
findPipelineGraph
=
()
=>
wrapper
.
find
(
'
[data-testid="graph-container"]
'
);
const
findAlert
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
const
findAlert
=
()
=>
wrapper
.
find
(
GlAlert
);
const
findAllJobPills
=
()
=>
wrapper
.
findAll
(
JobPill
);
const
findAllStagePills
=
()
=>
wrapper
.
findAll
(
StagePill
);
const
findAllStageBackgroundElements
=
()
=>
wrapper
.
findAll
(
'
[data-testid="stage-background"]
'
);
const
findAllStageBackgroundElements
=
()
=>
wrapper
.
findAll
(
'
[data-testid="stage-background"]
'
);
const
findAllStagePills
=
()
=>
wrapper
.
findAllComponents
(
StagePill
);
const
findLinksLayer
=
()
=>
wrapper
.
findComponent
(
LinksLayer
);
const
findPipelineGraph
=
()
=>
wrapper
.
find
(
'
[data-testid="graph-container"]
'
);
const
findStageBackgroundElementAt
=
(
index
)
=>
findAllStageBackgroundElements
().
at
(
index
);
const
findStageBackgroundElementAt
=
(
index
)
=>
findAllStageBackgroundElements
().
at
(
index
);
const
findAllJobPills
=
()
=>
wrapper
.
findAll
(
JobPill
);
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
wrapper
=
null
;
});
});
describe
(
'
with no data
'
,
()
=>
{
describe
(
'
with no data
'
,
()
=>
{
...
@@ -36,7 +47,7 @@ describe('pipeline graph component', () => {
...
@@ -36,7 +47,7 @@ describe('pipeline graph component', () => {
wrapper
=
createComponent
({
pipelineData
:
{}
});
wrapper
=
createComponent
({
pipelineData
:
{}
});
});
});
it
(
'
renders an empty section
'
,
()
=>
{
it
(
'
does not render the graph
'
,
()
=>
{
expect
(
wrapper
.
text
()).
toBe
(
wrapper
.
vm
.
$options
.
errorTexts
[
EMPTY_PIPELINE_DATA
]);
expect
(
wrapper
.
text
()).
toBe
(
wrapper
.
vm
.
$options
.
errorTexts
[
EMPTY_PIPELINE_DATA
]);
expect
(
findPipelineGraph
().
exists
()).
toBe
(
false
);
expect
(
findPipelineGraph
().
exists
()).
toBe
(
false
);
expect
(
findAllStagePills
()).
toHaveLength
(
0
);
expect
(
findAllStagePills
()).
toHaveLength
(
0
);
...
@@ -74,10 +85,11 @@ describe('pipeline graph component', () => {
...
@@ -74,10 +85,11 @@ describe('pipeline graph component', () => {
describe
(
'
with error while rendering the links with needs
'
,
()
=>
{
describe
(
'
with error while rendering the links with needs
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
();
wrapper
=
createComponent
(
{
pipelineData
:
invalidNeedsData
}
);
});
});
it
(
'
renders the error that link could not be drawn
'
,
()
=>
{
it
(
'
renders the error that link could not be drawn
'
,
()
=>
{
expect
(
findLinksLayer
().
exists
()).
toBe
(
true
);
expect
(
findAlert
().
exists
()).
toBe
(
true
);
expect
(
findAlert
().
exists
()).
toBe
(
true
);
expect
(
findAlert
().
text
()).
toBe
(
wrapper
.
vm
.
$options
.
errorTexts
[
DRAW_FAILURE
]);
expect
(
findAlert
().
text
()).
toBe
(
wrapper
.
vm
.
$options
.
errorTexts
[
DRAW_FAILURE
]);
});
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment