Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ The rules for this file:

<!-- New added features -->

- Added support to duplicate widgets (PR #13)

### Fixed

<!-- Bug fixes -->
Expand Down
12 changes: 12 additions & 0 deletions mdadash/backend/kernel/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,15 @@ def add_instance(self, data: dict) -> dict:
}
)

def duplicate_instance(self, data: dict) -> None:
"""Duplicate widget instance based on instance uuid"""
uid = data.get("uid")
new_uuid = self._wm.duplicate_widget_instance(uid, data.get("uuid"))
if self._um._connected:
# set the universe for the new widget instance
self._wm._set_universe(uid, self._um._universes[uid], new_uuid)
self._comm_handler.send({"status": "ok", "uuid": new_uuid})

def remove_instance(self, data: dict) -> dict:
"""Remove widget instance based on uuid"""
uuid = self._wm.delete_widget_instance(data.get("uuid", None))
Expand Down Expand Up @@ -350,6 +359,9 @@ def init_n_universes(data: dict) -> None:
"widgets:get_available_widgets", widgets_comm.get_available_widgets
)
comm_handler.register_handler("widgets:add_instance", widgets_comm.add_instance)
comm_handler.register_handler(
"widgets:duplicate_instance", widgets_comm.duplicate_instance
)
comm_handler.register_handler("widgets:remove_instance", widgets_comm.remove_instance)
comm_handler.register_handler("widget:get_inputs", widgets_comm.get_inputs)
comm_handler.register_handler("widget:set_input", widgets_comm.set_input)
27 changes: 27 additions & 0 deletions mdadash/backend/kernel/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,33 @@ async def add_widget_instance(self, uid: int, widget_name: str) -> dict:
"widgets:add_instance", {"uid": uid, "name": widget_name}
)

async def duplicate_widget_instance(self, uid: int, widget_uuid: str) -> dict:
"""Duplicate widget instance

Parameters
----------
uid: int
Universe ID (index into universes array)

widget_uuid: str
UUID of the widget instance to be duplicated

Returns
-------
response: dict
Response dict indicating status. This has the following keys:

status
String indication status: 'ok' or 'error'

message
An error message string when status is 'error'

"""
return await self.send_message_await_response(
"widgets:duplicate_instance", {"uid": uid, "uuid": widget_uuid}
)

async def remove_widget_instance(self, widget_uuid: str) -> dict:
"""Remove widget instance

Expand Down
23 changes: 22 additions & 1 deletion mdadash/backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,11 @@ async def remove_widget(_sid, uuid):
async def add_widget(_sid, uid, name, description):
response = await km.add_widget_instance(uid, name)
if response["status"] == "ok":
max_y = max(((w["y"] + w["h"]) for w in sm.widgets_layout), default=0)
sm.widgets_layout.append(
{
"x": 0,
"y": 0,
"y": max_y,
"w": 12,
"h": 14,
"i": response["uuid"],
Expand All @@ -168,6 +169,26 @@ async def add_widget(_sid, uid, name, description):
return response


@sio.on("widgets:duplicate_widget")
async def duplicate_widget(_sid, uid, uuid, name, description):
response = await km.duplicate_widget_instance(uid, uuid)
if response["status"] == "ok":
max_y = max(((w["y"] + w["h"]) for w in sm.widgets_layout), default=0)
sm.widgets_layout.append(
{
"x": 0,
"y": max_y,
"w": 12,
"h": 14,
"i": response["uuid"],
"name": f"Copy of {name}",
"description": description,
}
)
await emit_layout()
return response


@sio.on("dashboard:activated")
async def dashboard_activated(sid=None):
await km._emit_last_known_values(sid)
Expand Down
23 changes: 23 additions & 0 deletions mdadash/backend/tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,29 @@ async def test_add_remove_widgets(_client):
assert response["status"] == "ok"


async def test_duplicate_widgets(_client):
# add a widget
handler = sio.handlers["/"]["widgets:add_widget"]
response = await run_task_until_done(handler("_sid", 0, "Absolute Temperature", ""))
uuid1 = response.get("uuid", None)
assert uuid1 is not None
# duplicate the widget
handler = sio.handlers["/"]["widgets:duplicate_widget"]
response = await run_task_until_done(
handler("_sid", 0, uuid1, "Absolute Temperature", "")
)
uuid2 = response.get("uuid", None)
assert uuid2 is not None
# remove the original widget
handler = sio.handlers["/"]["widgets:remove_widget"]
response = await run_task_until_done(handler("_sid", uuid1))
assert response["status"] == "ok"
# remove the duplicate widget
handler = sio.handlers["/"]["widgets:remove_widget"]
response = await run_task_until_done(handler("_sid", uuid2))
assert response["status"] == "ok"


async def test_update_layout(_client):
handler = sio.handlers["/"]["widgets:update_layout"]
response = await run_task_until_done(handler("_sid", []))
Expand Down
37 changes: 36 additions & 1 deletion mdadash/backend/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,45 @@ def add_widget_instance(self, uid: int, widget_name: str) -> str | None:
return uuid
return None

def duplicate_widget_instance(self, uid: int, uuid: str) -> str | None:
"""Duplicate widget instance

Duplicate widget instance based on existing instance uuid

Parameters
----------
uid: int
Universe ID (index into universes array)

uuid: str
The uuid of the instance to be duplicated

Returns
-------
uuid of new instance created

"""
# get existing instance
instance = self.instances[uuid]
# duplicate instance
widget_class = instance.__class__
new_instance = widget_class()
setattr(new_instance, "uid", uid)
# set inputs for new instance
inputs = instance._get_inputs()
for _input in inputs:
attribute = _input["attribute"]
value = _input["value"]
setattr(new_instance, attribute, value)
# add new instance to instances list
new_uuid = str(uuid1())
self.instances[new_uuid] = instance
return new_uuid

def delete_widget_instance(self, uuid: str) -> str | None:
"""Remove widget instance

Remove widget instanced based on uuid returned during
Remove widget instance based on uuid returned during
the instance creation using :meth:`add_widget_instance`

Parameters
Expand Down
17 changes: 17 additions & 0 deletions mdadash/frontend/src/__tests__/views/DashboardView.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,24 @@ describe('DashboardView.vue', () => {
// click on edit
components[0].trigger('click')
// click on duplicate
mockEmitWithAck.mockResolvedValueOnce({ uuid: 'uuid2' })
components[1].trigger('click')
await nextTick()
expect(mockTimeout).toHaveBeenCalledWith(5000)
expect(mockEmitWithAck).toHaveBeenCalledWith(
'widgets:duplicate_widget',
0,
'uuid1',
'name1',
'desc1',
)
// check page moves to widget view
expect(mockPush).toHaveBeenCalledWith({
path: '/widget',
query: {
uuid: 'uuid2',
},
})
// click on the delete action
components[2].trigger('click')
// check remove widget sent to server
Expand Down
20 changes: 12 additions & 8 deletions mdadash/frontend/src/views/DashboardView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,9 @@ const gridPresetIcons = {
editable: () =>
h('svg', { viewBox: '0 0 24 24' }, [
h('path', {
d: 'M4 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5ZM14 5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V5ZM4 16a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-3ZM14 13a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-6Z',
fill: 'none',
d: 'M21 13.1C20.9 13.1 20.7 13.2 20.6 13.3L19.6 14.3L21.7 16.4L22.7 15.4C22.9 15.2 22.9 14.8 22.7 14.6L21.4 13.3C21.3 13.2 21.2 13.1 21 13.1M19.1 14.9L13 20.9V23H15.1L21.2 16.9L19.1 14.9M21 3H13V9H21V3M19 7H15V5H19V7M13 18.06V11H21V11.1C20.24 11.1 19.57 11.5 19.19 11.89L18.07 13H15V16.07L13 18.06M11 3H3V13H11V3M9 11H5V5H9V11M11 20.06V15H3V21H11V20.06M9 19H5V17H9V19Z',
stroke: '#000000',
'stroke-width': 2,
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': 0.1,
}),
]),
col1: () =>
Expand All @@ -322,8 +319,6 @@ const gridPresetIcons = {
fill: 'none',
stroke: '#000000',
'stroke-width': 2,
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
}),
]),
col2: () =>
Expand Down Expand Up @@ -510,7 +505,7 @@ async function handleAddWidgetClick(isOpen) {
}
}

function widgetFunction(item, action) {
async function widgetFunction(item, action) {
// Handle widget actions
if (action['title'] == 'Delete') {
socket.emit('widgets:remove_widget', item.i)
Expand All @@ -519,6 +514,15 @@ function widgetFunction(item, action) {
path: '/widget',
query: { uuid: item.i },
})
} else {
// (action['title'] == 'Duplicate')
const response = await socket
.timeout(settings.value.dashboard_config.ui_request_timeout)
.emitWithAck('widgets:duplicate_widget', 0, item.i, item.name, item.description)
router.push({
path: '/widget',
query: { uuid: response.uuid },
})
}
}

Expand Down
Loading