BigW Consortium Gitlab
Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
G
gitlab-ce
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Forest Godfrey
gitlab-ce
Commits
73723655
Commit
73723655
authored
Jan 24, 2018
by
Eric Eastwood
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Generalize toggle_buttons.js
Part of
https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110
parent
8e3f40f7
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
225 additions
and
155 deletions
+225
-155
clusters_bundle.js
app/assets/javascripts/clusters/clusters_bundle.js
+2
-10
clusters_index.js
app/assets/javascripts/clusters/clusters_index.js
+15
-53
toggle_buttons.js
app/assets/javascripts/toggle_buttons.js
+61
-0
_cluster.html.haml
app/views/projects/clusters/_cluster.html.haml
+3
-2
_integration_form.html.haml
app/views/projects/clusters/_integration_form.html.haml
+3
-4
gcp_spec.rb
spec/features/projects/clusters/gcp_spec.rb
+1
-1
user_spec.rb
spec/features/projects/clusters/user_spec.rb
+1
-1
clusters_spec.rb
spec/features/projects/clusters_spec.rb
+3
-3
clusters_bundle_spec.js
spec/javascripts/clusters/clusters_bundle_spec.js
+16
-8
clusters_index_spec.js
spec/javascripts/clusters/clusters_index_spec.js
+0
-58
clusters.rb
spec/javascripts/fixtures/clusters.rb
+0
-15
toggle_buttons_spec.js
spec/javascripts/toggle_buttons_spec.js
+120
-0
No files found.
app/assets/javascripts/clusters/clusters_bundle.js
View file @
73723655
...
...
@@ -14,6 +14,7 @@ import {
import
ClustersService
from
'./services/clusters_service'
;
import
ClustersStore
from
'./stores/clusters_store'
;
import
applications
from
'./components/applications.vue'
;
import
setupToggleButtons
from
'../toggle_buttons'
;
/**
* Cluster page has 2 separate parts:
...
...
@@ -48,12 +49,9 @@ export default class Clusters {
installPrometheusEndpoint
:
installPrometheusPath
,
});
this
.
toggle
=
this
.
toggle
.
bind
(
this
);
this
.
installApplication
=
this
.
installApplication
.
bind
(
this
);
this
.
showToken
=
this
.
showToken
.
bind
(
this
);
this
.
toggleButton
=
document
.
querySelector
(
'.js-toggle-cluster'
);
this
.
toggleInput
=
document
.
querySelector
(
'.js-toggle-input'
);
this
.
errorContainer
=
document
.
querySelector
(
'.js-cluster-error'
);
this
.
successContainer
=
document
.
querySelector
(
'.js-cluster-success'
);
this
.
creatingContainer
=
document
.
querySelector
(
'.js-cluster-creating'
);
...
...
@@ -63,6 +61,7 @@ export default class Clusters {
this
.
tokenField
=
document
.
querySelector
(
'.js-cluster-token'
);
initSettingsPanels
();
setupToggleButtons
(
document
.
querySelector
(
'.js-cluster-enable-toggle-area'
));
this
.
initApplications
();
if
(
this
.
store
.
state
.
status
!==
'created'
)
{
...
...
@@ -101,13 +100,11 @@ export default class Clusters {
}
addListeners
()
{
this
.
toggleButton
.
addEventListener
(
'click'
,
this
.
toggle
);
if
(
this
.
showTokenButton
)
this
.
showTokenButton
.
addEventListener
(
'click'
,
this
.
showToken
);
eventHub
.
$on
(
'installApplication'
,
this
.
installApplication
);
}
removeListeners
()
{
this
.
toggleButton
.
removeEventListener
(
'click'
,
this
.
toggle
);
if
(
this
.
showTokenButton
)
this
.
showTokenButton
.
removeEventListener
(
'click'
,
this
.
showToken
);
eventHub
.
$off
(
'installApplication'
,
this
.
installApplication
);
}
...
...
@@ -151,11 +148,6 @@ export default class Clusters {
this
.
updateContainer
(
prevStatus
,
this
.
store
.
state
.
status
,
this
.
store
.
state
.
statusReason
);
}
toggle
()
{
this
.
toggleButton
.
classList
.
toggle
(
'is-checked'
);
this
.
toggleInput
.
setAttribute
(
'value'
,
this
.
toggleButton
.
classList
.
contains
(
'is-checked'
).
toString
());
}
showToken
()
{
const
type
=
this
.
tokenField
.
getAttribute
(
'type'
);
...
...
app/assets/javascripts/clusters/clusters_index.js
View file @
73723655
import
Flash
from
'../flash'
;
import
{
s__
}
from
'../locale'
;
import
setupToggleButtons
from
'../toggle_buttons'
;
import
ClustersService
from
'./services/clusters_service'
;
/**
* Toggles loading and disabled classes.
* @param {HTMLElement} button
*/
const
toggleLoadingButton
=
(
button
)
=>
{
if
(
button
.
getAttribute
(
'disabled'
))
{
button
.
removeAttribute
(
'disabled'
);
}
else
{
button
.
setAttribute
(
'disabled'
,
true
);
}
button
.
classList
.
toggle
(
'is-loading'
);
};
/**
* Toggles checked class for the given button
* @param {HTMLElement} button
*/
const
toggleValue
=
(
button
)
=>
{
button
.
classList
.
toggle
(
'is-checked'
);
export
default
()
=>
{
const
clusterList
=
document
.
querySelector
(
'.js-clusters-list'
);
// The empty state won't have a clusterList
if
(
clusterList
)
{
setupToggleButtons
(
document
.
querySelector
(
'.js-clusters-list'
),
(
value
,
toggle
)
=>
ClustersService
.
updateCluster
(
toggle
.
dataset
.
endpoint
,
{
cluster
:
{
enabled
:
value
}
})
.
catch
((
err
)
=>
{
Flash
(
s__
(
'ClusterIntegration|Something went wrong on our end.'
));
throw
err
;
}),
);
}
};
/**
* Handles toggle buttons in the cluster's table.
*
* When the user clicks the toggle button for each cluster, it:
* - toggles the button
* - shows a loading and disables button
* - Makes a put request to the given endpoint
* Once we receive the response, either:
* 1) Show updated status in case of successfull response
* 2) Show initial status in case of failed response
*/
export
default
function
setClusterTableToggles
()
{
document
.
querySelectorAll
(
'.js-toggle-cluster-list'
)
.
forEach
(
button
=>
button
.
addEventListener
(
'click'
,
(
e
)
=>
{
const
toggleButton
=
e
.
currentTarget
;
const
endpoint
=
toggleButton
.
getAttribute
(
'data-endpoint'
);
toggleValue
(
toggleButton
);
toggleLoadingButton
(
toggleButton
);
const
value
=
toggleButton
.
classList
.
contains
(
'is-checked'
);
ClustersService
.
updateCluster
(
endpoint
,
{
cluster
:
{
enabled
:
value
}
})
.
then
(()
=>
{
toggleLoadingButton
(
toggleButton
);
})
.
catch
(()
=>
{
toggleLoadingButton
(
toggleButton
);
toggleValue
(
toggleButton
);
Flash
(
s__
(
'ClusterIntegration|Something went wrong on our end.'
));
});
}));
}
app/assets/javascripts/toggle_buttons.js
0 → 100644
View file @
73723655
import
$
from
'jquery'
;
import
Flash
from
'./flash'
;
import
{
__
}
from
'./locale'
;
import
{
convertPermissionToBoolean
}
from
'./lib/utils/common_utils'
;
/*
example HAML:
```
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if enabled?}",
'aria-label': _('Toggle Cluster') }
%input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? }
```
*/
function
updatetoggle
(
toggle
,
isOn
)
{
toggle
.
classList
.
toggle
(
'is-checked'
,
isOn
);
}
function
onToggleClicked
(
toggle
,
input
,
clickCallback
)
{
const
previousIsOn
=
convertPermissionToBoolean
(
input
.
value
);
// Visually change the toggle and start loading
updatetoggle
(
toggle
,
!
previousIsOn
);
toggle
.
setAttribute
(
'disabled'
,
true
);
toggle
.
classList
.
toggle
(
'is-loading'
,
true
);
Promise
.
resolve
(
clickCallback
(
!
previousIsOn
,
toggle
))
.
then
(()
=>
{
// Actually change the input value
input
.
setAttribute
(
'value'
,
!
previousIsOn
);
})
.
catch
(()
=>
{
// Revert the visuals if something goes wrong
updatetoggle
(
toggle
,
previousIsOn
);
})
.
then
(()
=>
{
// Remove the loading indicator in any case
toggle
.
removeAttribute
(
'disabled'
);
toggle
.
classList
.
toggle
(
'is-loading'
,
false
);
$
(
input
).
trigger
(
'trigger-change'
);
})
.
catch
(()
=>
{
Flash
(
__
(
'Something went wrong when toggling the button'
));
});
}
export
default
function
setupToggleButtons
(
container
,
clickCallback
=
()
=>
{})
{
const
toggles
=
container
.
querySelectorAll
(
'.js-project-feature-toggle'
);
toggles
.
forEach
((
toggle
)
=>
{
const
input
=
toggle
.
querySelector
(
'.js-project-feature-toggle-input'
);
const
isOn
=
convertPermissionToBoolean
(
input
.
value
);
// Get the visible toggle in sync with the hidden input
updatetoggle
(
toggle
,
isOn
);
toggle
.
addEventListener
(
'click'
,
onToggleClicked
.
bind
(
null
,
toggle
,
input
,
clickCallback
));
});
}
app/views/projects/clusters/_cluster.html.haml
View file @
73723655
...
...
@@ -12,11 +12,12 @@
.table-section.section-10
.table-mobile-header
{
role:
"rowheader"
}
.table-mobile-content
%button
{
type:
"button"
,
class:
"
js-toggle-cluster-list project-feature-toggle
#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}"
,
%button
.js-project-feature-toggle.project-feature-toggle
{
type:
"button"
,
class:
"#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}"
,
"aria-label"
:
s_
(
"ClusterIntegration|Toggle Cluster"
),
disabled:
!
cluster
.
can_toggle_cluster?
,
data:
{
endpoint:
namespace_project_cluster_path
(
@project
.
namespace
,
@project
,
cluster
,
format: :json
)
}
}
%input
.js-project-feature-toggle-input
{
type:
"hidden"
,
value:
cluster
.
enabled?
}
=
icon
(
"spinner spin"
,
class:
"loading-icon"
)
%span
.toggle-icon
=
sprite_icon
(
'status_success_borderless'
,
size:
16
,
css_class:
'toggle-icon-svg toggle-status-checked'
)
...
...
app/views/projects/clusters/_integration_form.html.haml
View file @
73723655
...
...
@@ -10,13 +10,12 @@
=
s_
(
'ClusterIntegration|Cluster integration is enabled for this project.'
)
-
else
=
s_
(
'ClusterIntegration|Cluster integration is disabled for this project.'
)
%label
.append-bottom-10
=
field
.
hidden_field
:enabled
,
{
class:
'js-toggle-input'
}
%label
.append-bottom-10.js-cluster-enable-toggle-area
%button
{
type:
'button'
,
class:
"js-
toggle-cluster project-feature-toggle #{'is-checked' unless !
@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}"
,
class:
"js-
project-feature-toggle project-feature-toggle #{'is-checked' if
@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}"
,
"aria-label"
:
s_
(
"ClusterIntegration|Toggle Cluster"
),
disabled:
!
can?
(
current_user
,
:update_cluster
,
@cluster
)
}
=
field
.
hidden_field
:enabled
,
{
class:
'js-project-feature-toggle-input'
}
%span
.toggle-icon
=
sprite_icon
(
'status_success_borderless'
,
size:
16
,
css_class:
'toggle-icon-svg toggle-status-checked'
)
=
sprite_icon
(
'status_failed_borderless'
,
size:
16
,
css_class:
'toggle-icon-svg toggle-status-unchecked'
)
...
...
spec/features/projects/clusters/gcp_spec.rb
View file @
73723655
...
...
@@ -95,7 +95,7 @@ feature 'Gcp Cluster', :js do
context
'when user disables the cluster'
do
before
do
page
.
find
(
:css
,
'.js-
toggle-cluster
'
).
click
page
.
find
(
:css
,
'.js-
cluster-enable-toggle-area .js-project-feature-toggle
'
).
click
page
.
within
(
'#cluster-integration'
)
{
click_button
'Save changes'
}
end
...
...
spec/features/projects/clusters/user_spec.rb
View file @
73723655
...
...
@@ -62,7 +62,7 @@ feature 'User Cluster', :js do
context
'when user disables the cluster'
do
before
do
page
.
find
(
:css
,
'.js-
toggle-cluster
'
).
click
page
.
find
(
:css
,
'.js-
cluster-enable-toggle-area .js-project-feature-toggle
'
).
click
fill_in
'cluster_name'
,
with:
'dev-cluster'
page
.
within
(
'#cluster-integration'
)
{
click_button
'Save changes'
}
end
...
...
spec/features/projects/clusters_spec.rb
View file @
73723655
...
...
@@ -37,13 +37,13 @@ feature 'Clusters', :js do
context
'inline update of cluster'
do
it
'user can update cluster'
do
expect
(
page
).
to
have_selector
(
'.js-
toggle-cluster-list
'
)
expect
(
page
).
to
have_selector
(
'.js-
project-feature-toggle
'
)
end
context
'with sucessfull request'
do
it
'user sees updated cluster'
do
expect
do
page
.
find
(
'.js-
toggle-cluster-list
'
).
click
page
.
find
(
'.js-
project-feature-toggle
'
).
click
wait_for_requests
end
.
to
change
{
cluster
.
reload
.
enabled
}
...
...
@@ -57,7 +57,7 @@ feature 'Clusters', :js do
expect_any_instance_of
(
Clusters
::
UpdateService
).
to
receive
(
:execute
).
and_call_original
allow_any_instance_of
(
Clusters
::
Cluster
).
to
receive
(
:valid?
)
{
false
}
page
.
find
(
'.js-
toggle-cluster-list
'
).
click
page
.
find
(
'.js-
project-feature-toggle
'
).
click
expect
(
page
).
to
have_content
(
'Something went wrong on our end.'
)
expect
(
page
).
to
have_selector
(
'.is-checked'
)
...
...
spec/javascripts/clusters/clusters_bundle_spec.js
View file @
73723655
...
...
@@ -23,16 +23,24 @@ describe('Clusters', () => {
});
describe
(
'toggle'
,
()
=>
{
it
(
'should update the button and the input field on click'
,
()
=>
{
cluster
.
toggleButton
.
click
();
it
(
'should update the button and the input field on click'
,
(
done
)
=>
{
const
toggleButton
=
document
.
querySelector
(
'.js-cluster-enable-toggle-area .js-project-feature-toggle'
);
const
toggleInput
=
document
.
querySelector
(
'.js-cluster-enable-toggle-area .js-project-feature-toggle-input'
);
expect
(
cluster
.
toggleButton
.
classList
,
).
not
.
toContain
(
'is-checked'
);
toggleButton
.
click
();
expect
(
cluster
.
toggleInput
.
getAttribute
(
'value'
),
).
toEqual
(
'false'
);
getSetTimeoutPromise
()
.
then
(()
=>
{
expect
(
toggleButton
.
classList
,
).
not
.
toContain
(
'is-checked'
);
expect
(
toggleInput
.
getAttribute
(
'value'
),
).
toEqual
(
'false'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
...
...
spec/javascripts/clusters/clusters_index_spec.js
deleted
100644 → 0
View file @
8e3f40f7
import
MockAdapter
from
'axios-mock-adapter'
;
import
axios
from
'~/lib/utils/axios_utils'
;
import
setClusterTableToggles
from
'~/clusters/clusters_index'
;
import
{
setTimeout
}
from
'core-js/library/web/timers'
;
describe
(
'Clusters table'
,
()
=>
{
preloadFixtures
(
'clusters/index_cluster.html.raw'
);
let
mock
;
beforeEach
(()
=>
{
loadFixtures
(
'clusters/index_cluster.html.raw'
);
mock
=
new
MockAdapter
(
axios
);
setClusterTableToggles
();
});
describe
(
'update cluster'
,
()
=>
{
it
(
'renders loading state while request is made'
,
()
=>
{
const
button
=
document
.
querySelector
(
'.js-toggle-cluster-list'
);
button
.
click
();
expect
(
button
.
classList
).
toContain
(
'is-loading'
);
expect
(
button
.
getAttribute
(
'disabled'
)).
toEqual
(
'true'
);
});
afterEach
(()
=>
{
mock
.
restore
();
});
it
(
'shows updated state after sucessfull request'
,
(
done
)
=>
{
mock
.
onPut
().
reply
(
200
,
{},
{});
const
button
=
document
.
querySelector
(
'.js-toggle-cluster-list'
);
button
.
click
();
expect
(
button
.
classList
).
toContain
(
'is-loading'
);
setTimeout
(()
=>
{
expect
(
button
.
classList
).
not
.
toContain
(
'is-loading'
);
expect
(
button
.
classList
).
not
.
toContain
(
'is-checked'
);
done
();
},
0
);
});
it
(
'shows inital state after failed request'
,
(
done
)
=>
{
mock
.
onPut
().
reply
(
500
,
{},
{});
const
button
=
document
.
querySelector
(
'.js-toggle-cluster-list'
);
button
.
click
();
expect
(
button
.
classList
).
toContain
(
'is-loading'
);
setTimeout
(()
=>
{
expect
(
button
.
classList
).
not
.
toContain
(
'is-loading'
);
expect
(
button
.
classList
).
toContain
(
'is-checked'
);
done
();
},
0
);
});
});
});
spec/javascripts/fixtures/clusters.rb
View file @
73723655
...
...
@@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
expect
(
response
).
to
be_success
store_frontend_fixture
(
response
,
example
.
description
)
end
context
'rendering non-empty state'
do
before
do
cluster
end
it
'clusters/index_cluster.html.raw'
do
|
example
|
get
:index
,
namespace_id:
namespace
,
project_id:
project
expect
(
response
).
to
be_success
store_frontend_fixture
(
response
,
example
.
description
)
end
end
end
spec/javascripts/toggle_buttons_spec.js
0 → 100644
View file @
73723655
import
setupToggleButtons
from
'~/toggle_buttons'
;
import
getSetTimeoutPromise
from
'./helpers/set_timeout_promise_helper'
;
function
generateMarkup
(
isChecked
=
true
)
{
return
`
<button type="button" class="
${
isChecked
?
'is-checked'
:
''
}
js-project-feature-toggle">
<input type="hidden" class="js-project-feature-toggle-input" value="
${
isChecked
}
" />
</button>
`
;
}
function
setupFixture
(
isChecked
,
clickCallback
)
{
const
wrapper
=
document
.
createElement
(
'div'
);
wrapper
.
innerHTML
=
generateMarkup
(
isChecked
);
setupToggleButtons
(
wrapper
,
clickCallback
);
return
wrapper
;
}
describe
(
'ToggleButtons'
,
()
=>
{
describe
(
'when input value is true'
,
()
=>
{
it
(
'should initialize as checked'
,
()
=>
{
const
wrapper
=
setupFixture
(
true
);
expect
(
wrapper
.
querySelector
(
'.js-project-feature-toggle'
).
classList
.
contains
(
'is-checked'
)).
toEqual
(
true
);
expect
(
wrapper
.
querySelector
(
'.js-project-feature-toggle-input'
).
value
).
toEqual
(
'true'
);
});
it
(
'should toggle to unchecked when clicked'
,
(
done
)
=>
{
const
wrapper
=
setupFixture
(
true
);
const
toggleButton
=
wrapper
.
querySelector
(
'.js-project-feature-toggle'
);
toggleButton
.
click
();
getSetTimeoutPromise
()
.
then
(()
=>
{
expect
(
toggleButton
.
classList
.
contains
(
'is-checked'
)).
toEqual
(
false
);
expect
(
wrapper
.
querySelector
(
'.js-project-feature-toggle-input'
).
value
).
toEqual
(
'false'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
describe
(
'when input value is false'
,
()
=>
{
it
(
'should initialize as unchecked'
,
()
=>
{
const
wrapper
=
setupFixture
(
false
);
expect
(
wrapper
.
querySelector
(
'.js-project-feature-toggle'
).
classList
.
contains
(
'is-checked'
)).
toEqual
(
false
);
expect
(
wrapper
.
querySelector
(
'.js-project-feature-toggle-input'
).
value
).
toEqual
(
'false'
);
});
it
(
'should toggle to checked when clicked'
,
(
done
)
=>
{
const
wrapper
=
setupFixture
(
false
);
const
toggleButton
=
wrapper
.
querySelector
(
'.js-project-feature-toggle'
);
toggleButton
.
click
();
getSetTimeoutPromise
()
.
then
(()
=>
{
expect
(
toggleButton
.
classList
.
contains
(
'is-checked'
)).
toEqual
(
true
);
expect
(
wrapper
.
querySelector
(
'.js-project-feature-toggle-input'
).
value
).
toEqual
(
'true'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
it
(
'should emit `trigger-change` event'
,
(
done
)
=>
{
const
changeSpy
=
jasmine
.
createSpy
(
'changeEventHandler'
);
const
wrapper
=
setupFixture
(
false
);
const
toggleButton
=
wrapper
.
querySelector
(
'.js-project-feature-toggle'
);
const
input
=
wrapper
.
querySelector
(
'.js-project-feature-toggle-input'
);
$
(
input
).
on
(
'trigger-change'
,
changeSpy
);
toggleButton
.
click
();
getSetTimeoutPromise
()
.
then
(()
=>
{
expect
(
changeSpy
).
toHaveBeenCalled
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
describe
(
'clickCallback'
,
()
=>
{
it
(
'should show loading indicator while waiting'
,
(
done
)
=>
{
const
isChecked
=
true
;
const
clickCallback
=
(
newValue
,
toggleButton
)
=>
{
const
input
=
toggleButton
.
querySelector
(
'.js-project-feature-toggle-input'
);
expect
(
newValue
).
toEqual
(
false
);
// Check for the loading state
expect
(
toggleButton
.
classList
.
contains
(
'is-checked'
)).
toEqual
(
false
);
expect
(
toggleButton
.
classList
.
contains
(
'is-loading'
)).
toEqual
(
true
);
expect
(
toggleButton
.
disabled
).
toEqual
(
true
);
expect
(
input
.
value
).
toEqual
(
'true'
);
// After the callback finishes, check that the loading state is gone
getSetTimeoutPromise
()
.
then
(()
=>
{
expect
(
toggleButton
.
classList
.
contains
(
'is-checked'
)).
toEqual
(
false
);
expect
(
toggleButton
.
classList
.
contains
(
'is-loading'
)).
toEqual
(
false
);
expect
(
toggleButton
.
disabled
).
toEqual
(
false
);
expect
(
input
.
value
).
toEqual
(
'false'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
};
const
wrapper
=
setupFixture
(
isChecked
,
clickCallback
);
const
toggleButton
=
wrapper
.
querySelector
(
'.js-project-feature-toggle'
);
toggleButton
.
click
();
});
});
});
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