Skip to content

Commit a0be526

Browse files
authored
Componentize to simplify dashboard (#10)
This refactors the dashboard to split parts of it out into new components to better organize the mass of code. It was driving me nuts. I encapsulated the following sections: - The workspaces list items - The button toolbar - The map preview - The details table Along the way, I addressed a logic issue with the map preview that caused bug 1833. The map now displays a bounding box when the TDEI metadata contains an invalid `dataset_area` polygon.
2 parents bb61484 + 76a62d3 commit a0be526

File tree

5 files changed

+321
-229
lines changed

5 files changed

+321
-229
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<template>
2+
<div class="table-responsive border-top">
3+
<table class="table table-striped mb-0">
4+
<tbody>
5+
<tr>
6+
<th><app-icon variant="schedule" />Created At</th>
7+
<td>{{ workspace.createdAt?.toLocaleString() }}</td>
8+
</tr>
9+
<tr>
10+
<th><app-icon variant="person_outline" />Created By</th>
11+
<td>{{ workspace.createdByName }}</td>
12+
</tr>
13+
<tr>
14+
<th><app-icon variant="phonelink_setup" />App Access</th>
15+
<td>
16+
<span v-if="workspace.externalAppAccess === 0" class="badge bg-secondary text-uppercase">
17+
Disabled
18+
</span>
19+
<span v-else-if="workspace.externalAppAccess === 1" class="badge bg-success text-uppercase">
20+
Public
21+
</span>
22+
<span v-else-if="workspace.externalAppAccess === 2" class="badge bg-success text-uppercase">
23+
Project Group Only
24+
</span>
25+
</td>
26+
</tr>
27+
<tr>
28+
<th><app-icon variant="dataset" />From TDEI Dataset ID</th>
29+
<td>{{ workspace.tdeiRecordId ?? 'N/A' }}</td>
30+
</tr>
31+
<tr>
32+
<th><app-icon variant="group_work" />TDEI Project Group ID</th>
33+
<td>{{ workspace.tdeiProjectGroupId }}</td>
34+
</tr>
35+
<tr>
36+
<th><app-icon variant="update" />TDEI Dataset Version</th>
37+
<td>{{ workspace.tdeiMetadata?.metadata?.dataset_detail?.version ?? "N/A" }}</td>
38+
</tr>
39+
</tbody>
40+
</table>
41+
</div><!-- .table-responsive -->
42+
</template>
43+
44+
<script setup lang="ts">
45+
const props = defineProps({
46+
workspace: {
47+
type: Object,
48+
required: true
49+
}
50+
});
51+
</script>
52+
53+
<style lang="scss">
54+
.dashboard-page {
55+
table th {
56+
white-space: nowrap;
57+
}
58+
}
59+
</style>

components/dashboard/Map.vue

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<template>
2+
<div class="map-container">
3+
<div v-show="workspaceAreaPolygon" id="map" />
4+
<div v-show="!workspaceAreaPolygon" class="missing-workspace-area-notice">
5+
<loading-spinner v-if="loadingBbox.active" />
6+
<template v-else>
7+
<app-icon variant="info" size="48" />
8+
<div>This workspace is empty.</div>
9+
</template>
10+
</div>
11+
</div>
12+
</template>
13+
14+
<script setup lang="ts">
15+
import { LoadingContext } from '~/services/loading';
16+
import { workspacesClient } from '~/services/index';
17+
18+
const props = defineProps({
19+
workspace: {
20+
type: Object,
21+
default: null
22+
}
23+
});
24+
25+
const emit = defineEmits(['centerLoaded']);
26+
27+
const loadingBbox = reactive(new LoadingContext());
28+
const map = ref(null);
29+
const workspaceAreaPolygon = ref(null);
30+
31+
onMounted(() => {
32+
watch(
33+
() => props.workspace,
34+
(val) => {
35+
if (val) {
36+
updateMapPreview(val);
37+
} else {
38+
map.value = null;
39+
}
40+
},
41+
{ immediate: true }
42+
);
43+
});
44+
45+
function initMap() {
46+
// TODO: use Mapbox
47+
map.value = L.map('map');
48+
49+
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
50+
maxZoom: 19,
51+
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
52+
}).addTo(map.value);
53+
}
54+
55+
async function updateMapPreview(workspace) {
56+
if (workspaceAreaPolygon.value) {
57+
workspaceAreaPolygon.value.remove();
58+
workspaceAreaPolygon.value = null
59+
}
60+
61+
if (!props.workspace.id) {
62+
return;
63+
}
64+
65+
await setCurrentWorkspacePolygon(workspace);
66+
67+
if (!workspaceAreaPolygon.value) {
68+
return;
69+
}
70+
71+
if (!map.value) {
72+
initMap();
73+
}
74+
75+
const bounds = workspaceAreaPolygon.value.getBounds();
76+
77+
workspaceAreaPolygon.value.addTo(map.value);
78+
map.value.fitBounds(bounds);
79+
80+
const zoom = map.value.getBoundsZoom(bounds);
81+
const center = bounds.getCenter();
82+
83+
emit('centerLoaded', { zoom, latitude: center.lat, longitude: center.lng });
84+
}
85+
86+
async function setCurrentWorkspacePolygon(workspace) {
87+
const metadataArea = workspace.tdeiMetadata?.metadata?.dataset_detail?.dataset_area;
88+
89+
if (metadataArea) {
90+
const polygon = L.geoJSON(metadataArea);
91+
92+
if (polygon.getBounds().isValid()) {
93+
workspaceAreaPolygon.value = polygon;
94+
return;
95+
}
96+
}
97+
98+
await loadingBbox.cancelable(workspacesClient, async (client) => {
99+
const bbox = await client.getWorkspaceBbox(workspace.id);
100+
101+
if (bbox) {
102+
workspaceAreaPolygon.value = L.rectangle([
103+
[bbox.min_lat, bbox.min_lon],
104+
[bbox.max_lat, bbox.max_lon]
105+
])
106+
}
107+
});
108+
}
109+
</script>
110+
111+
<style lang="scss">
112+
@import "assets/scss/theme.scss";
113+
114+
.dashboard-page {
115+
.map-container {
116+
height: 350px;
117+
background-color: $gray-200;
118+
}
119+
120+
#map {
121+
width: 100%;
122+
height: 100%;
123+
}
124+
125+
.missing-workspace-area-notice {
126+
width: 100%;
127+
height: 100%;
128+
display: flex;
129+
flex-direction: column;
130+
align-items: center;
131+
justify-content: center;
132+
color: $gray-600;
133+
text-align: center;
134+
}
135+
}
136+
</style>

components/dashboard/Toolbar.vue

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<template>
2+
<div class="btn-toolbar">
3+
<nuxt-link class="btn btn-dark" :to="editRoute">
4+
<app-icon variant="edit_location_alt" size="24" />
5+
Edit
6+
</nuxt-link>
7+
<div class="btn-group">
8+
<!--
9+
<a :href="tasksHref" class="btn" target="_blank">
10+
<app-icon variant="checklist" size="24" />
11+
<span class="d-none d-sm-inline">Tasks</span>
12+
</a>
13+
-->
14+
<nuxt-link class="btn" :to="exportRoute" :aria-disabled="!workspace.center">
15+
<app-icon variant="drive_folder_upload" size="24" />
16+
<span class="d-none d-sm-inline">Export</span>
17+
</nuxt-link>
18+
</div>
19+
<div class="btn-group ms-auto">
20+
<nuxt-link class="btn" :to="settingsRoute">
21+
<app-icon variant="settings" size="24" />
22+
<span class="d-none d-sm-inline">Settings</span>
23+
</nuxt-link>
24+
</div>
25+
</div>
26+
</template>
27+
28+
<script setup lang="ts">
29+
const props = defineProps({
30+
workspace: {
31+
type: Object,
32+
required: true
33+
}
34+
});
35+
36+
const editHash = computed(() => {
37+
if (!props.workspace.center) {
38+
return undefined;
39+
}
40+
41+
const { zoom, latitude, longitude } = props.workspace.center;
42+
43+
return `#map=${zoom}/${latitude}/${longitude}`;
44+
});
45+
46+
const editRoute = computed(() => ({
47+
path: workspacePath('edit'),
48+
query: { datatype: props.workspace.type },
49+
hash: editHash.value
50+
}));
51+
52+
const exportRoute = computed(() => workspacePath('export'));
53+
const settingsRoute = computed(() => workspacePath('settings'));
54+
55+
function workspacePath(page) {
56+
return `/workspace/${props.workspace.id}/${page}`;
57+
}
58+
</script>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<template>
2+
<button :class="getClasses(workspace)">
3+
<div class="fw-bold">{{ workspace.title }}</div>
4+
5+
<span class="badge bg-secondary"><app-icon variant="insert_drive_file" />{{ workspace.type }}</span>
6+
7+
<span v-if="workspace.externalAppAccess > 0" class="badge bg-success ms-2">
8+
<app-icon v-if="workspace.externalAppAccess === 1" variant="public" />
9+
<app-icon v-else variant="lock" />
10+
App
11+
</span>
12+
</button>
13+
</template>
14+
15+
<script setup lang="ts">
16+
const props = defineProps({
17+
workspace: {
18+
type: Object,
19+
required: true
20+
},
21+
selected: {
22+
type: Boolean,
23+
default: false
24+
}
25+
});
26+
27+
function getClasses(workspace) {
28+
return {
29+
'list-group-item': true,
30+
'list-group-item-action': true,
31+
'active': props.selected
32+
};
33+
}
34+
</script>
35+
36+
<style lang="scss">
37+
.dashboard-page {
38+
.list-group-item {
39+
cursor: pointer;
40+
41+
&.active {
42+
position: sticky;
43+
top: 1rem;
44+
bottom: 1rem;
45+
}
46+
47+
.badge {
48+
text-transform: uppercase;
49+
}
50+
}
51+
}
52+
</style>

0 commit comments

Comments
 (0)