mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
update
This commit is contained in:
@@ -76,7 +76,6 @@ async function confirmSnapshot() {
|
||||
</div>
|
||||
|
||||
<div class="mt-4 justify-center flex w-full">
|
||||
|
||||
<UPopover class="w-full" :popper="{ placement: 'bottom' }">
|
||||
<UButton class="w-full" color="primary" variant="solid">
|
||||
<div class="flex items-center justify-center w-full gap-2">
|
||||
@@ -97,8 +96,6 @@ async function confirmSnapshot() {
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="grow"></div>
|
||||
|
||||
157
dashboard/components/integrations/SupabaseChartDialog.vue
Normal file
157
dashboard/components/integrations/SupabaseChartDialog.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
import { sub, isSameDay, type Duration } from 'date-fns'
|
||||
|
||||
type ChartType = 'bar' | 'line';
|
||||
const chartTypeOptions: { value: ChartType, label: string }[] = [
|
||||
{ value: 'bar', label: 'Bar chart' },
|
||||
{ value: 'line', label: 'Line chart' },
|
||||
]
|
||||
|
||||
type yAxisMode = 'count';
|
||||
const yAxisModeOptions: { value: yAxisMode, label: string }[] = [
|
||||
{ value: 'count', label: 'Count fields' },
|
||||
]
|
||||
|
||||
type Slice = 'day' | 'month';
|
||||
const sliceOptions: Slice[] = ['day', 'month'];
|
||||
|
||||
const chartType = ref<ChartType>('line');
|
||||
const tableName = ref<string>('');
|
||||
const xAxis = ref<string>('');
|
||||
const yAxisMode = ref<yAxisMode>('count');
|
||||
const slice = ref<Slice>('day');
|
||||
const visualizationName = ref<string>('');
|
||||
|
||||
|
||||
const ranges = [
|
||||
{ label: 'Last 7 days', duration: { days: 7 } },
|
||||
{ label: 'Last 14 days', duration: { days: 14 } },
|
||||
{ label: 'Last 30 days', duration: { days: 30 } },
|
||||
{ label: 'Last 3 months', duration: { months: 3 } },
|
||||
{ label: 'Last 6 months', duration: { months: 6 } },
|
||||
{ label: 'Last year', duration: { years: 1 } }
|
||||
]
|
||||
const timeframe = ref<{ start: Date, end: Date }>({ start: sub(new Date(), { days: 14 }), end: new Date() })
|
||||
|
||||
function isRangeSelected(duration: Duration) {
|
||||
return isSameDay(timeframe.value.start, sub(new Date(), duration)) && isSameDay(timeframe.value.end, new Date())
|
||||
}
|
||||
|
||||
function selectRange(duration: Duration) {
|
||||
timeframe.value = { start: sub(new Date(), duration), end: new Date() }
|
||||
}
|
||||
|
||||
const { createAlert } = useAlert();
|
||||
const { closeDialog } = useCustomDialog();
|
||||
const activeProjectId = useActiveProjectId();
|
||||
|
||||
const { integrationsCredentials,testConnection } = useSupabase();
|
||||
|
||||
async function generate() {
|
||||
const credentials = integrationsCredentials.data.value;
|
||||
if (!credentials?.supabase) return createAlert('Credentials not found', 'Please add supabase credentials on the integration page', 'far fa-error', 5000);
|
||||
const connectionStatus = await testConnection();
|
||||
if (!connectionStatus) return createAlert('Invalid supabase credentials', 'Please check your supabase credentials on the integration page', 'far fa-error', 5000);
|
||||
|
||||
try {
|
||||
const creation = await $fetch('/api/integrations/supabase/add', {
|
||||
...signHeaders({
|
||||
'x-pid': activeProjectId.data.value || '',
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: visualizationName.value,
|
||||
chart_type: chartType.value,
|
||||
table_name: tableName.value,
|
||||
xField: xAxis.value,
|
||||
yMode: yAxisMode.value,
|
||||
from: timeframe.value.start,
|
||||
to: timeframe.value.end,
|
||||
slice: slice.value
|
||||
})
|
||||
})
|
||||
|
||||
createAlert('Integration generated', 'Integration generated successfully', 'far fa-check-circle', 5000);
|
||||
closeDialog();
|
||||
} catch (ex: any) {
|
||||
createAlert('Error generating integrations', ex.response._data.message.toString(), 'far fa-error', 5000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<div> Visualization name </div>
|
||||
<div>
|
||||
<LyxUiInput class="w-full px-2 py-1" v-model="visualizationName"></LyxUiInput>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div> Chart type </div>
|
||||
<USelect v-model="chartType" :options="chartTypeOptions" />
|
||||
</div>
|
||||
<div>
|
||||
<div> Table name </div>
|
||||
<div>
|
||||
<LyxUiInput class="w-full px-2 py-1" v-model="tableName"></LyxUiInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div> X axis field </div>
|
||||
<div>
|
||||
<LyxUiInput class="w-full px-2 py-1" v-model="xAxis"></LyxUiInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div> Y axis mode </div>
|
||||
<div>
|
||||
<USelect v-model="yAxisMode" :options="yAxisModeOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div> Timeframe </div>
|
||||
<div>
|
||||
<UPopover class="w-full" :popper="{ placement: 'bottom' }">
|
||||
<UButton class="w-full" color="primary" variant="solid">
|
||||
<div class="flex items-center justify-center w-full gap-2">
|
||||
<i class="i-heroicons-calendar-days-20-solid"></i>
|
||||
{{ timeframe.start.toLocaleDateString() }} - {{ timeframe.end.toLocaleDateString() }}
|
||||
</div>
|
||||
</UButton>
|
||||
<template #panel="{ close }">
|
||||
<div class="flex items-center sm:divide-x divide-gray-200 dark:divide-gray-800">
|
||||
<div class="hidden sm:flex flex-col py-4">
|
||||
<UButton v-for="(range, index) in ranges" :key="index" :label="range.label" color="gray"
|
||||
variant="ghost" class="rounded-none px-6"
|
||||
:class="[isRangeSelected(range.duration) ? 'bg-gray-100 dark:bg-gray-800' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50']"
|
||||
truncate @click="selectRange(range.duration)" />
|
||||
</div>
|
||||
|
||||
<DatePicker v-model="timeframe" @close="close" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div> View mode </div>
|
||||
<div>
|
||||
<USelect v-model="slice" :options="sliceOptions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LyxUiButton type="primary" @click="generate()">
|
||||
Generate
|
||||
</LyxUiButton>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
170
dashboard/components/integrations/SupabaseLineChart.vue
Normal file
170
dashboard/components/integrations/SupabaseLineChart.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script setup lang="ts">
|
||||
import type { TSupabaseIntegration } from '@schema/integrations/SupabaseIntegrationSchema';
|
||||
import type { ChartData, ChartOptions } from 'chart.js';
|
||||
import { useLineChart, LineChart } from 'vue-chart-3';
|
||||
|
||||
const props = defineProps<{ integration_id: string }>();
|
||||
|
||||
const activeProjectId = useActiveProjectId();
|
||||
|
||||
const supabaseData = ref<{ labels: string[], data: number[] }>();
|
||||
const supabaseError = ref<string | undefined>(undefined);
|
||||
const supabaseFetching = ref<boolean>(false);
|
||||
|
||||
const { getRemoteData } = useSupabase();
|
||||
|
||||
function createGradient() {
|
||||
|
||||
const c = document.createElement('canvas');
|
||||
const ctx = c.getContext("2d");
|
||||
let gradient: any = `#34B67C22`;
|
||||
if (ctx) {
|
||||
gradient = ctx.createLinearGradient(0, 25, 0, 300);
|
||||
gradient.addColorStop(0, `#34B67C99`);
|
||||
gradient.addColorStop(0.35, `#34B67C66`);
|
||||
gradient.addColorStop(1, `#34B67C22`);
|
||||
} else {
|
||||
console.warn('Cannot get context for gradient');
|
||||
}
|
||||
|
||||
chartData.value.datasets[0].backgroundColor = [gradient];
|
||||
}
|
||||
|
||||
|
||||
|
||||
const chartOptions = ref<ChartOptions<'line'>>({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'nearest',
|
||||
axis: 'x',
|
||||
includeInvisible: true
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
ticks: { display: true },
|
||||
grid: {
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
color: '#CCCCCC22',
|
||||
// borderDash: [5, 10]
|
||||
},
|
||||
},
|
||||
x: {
|
||||
ticks: { display: true },
|
||||
grid: {
|
||||
display: true,
|
||||
drawBorder: false,
|
||||
color: '#CCCCCC22',
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
titleFont: { size: 16, weight: 'bold' },
|
||||
bodyFont: { size: 14 },
|
||||
padding: 10,
|
||||
cornerRadius: 4,
|
||||
boxPadding: 10,
|
||||
caretPadding: 20,
|
||||
yAlign: 'bottom',
|
||||
xAlign: 'center',
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const chartData = ref<ChartData<'line'>>({
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
data: [],
|
||||
backgroundColor: ['#34B67C' + '77'],
|
||||
borderColor: '#34B67C',
|
||||
borderWidth: 4,
|
||||
fill: true,
|
||||
tension: 0.45,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 10,
|
||||
hoverBackgroundColor: '#34B67C',
|
||||
hoverBorderColor: 'white',
|
||||
hoverBorderWidth: 2,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
supabaseFetching.value = true;
|
||||
supabaseError.value = undefined;
|
||||
|
||||
const integrationData = await $fetch<TSupabaseIntegration>('/api/integrations/supabase/get', {
|
||||
...signHeaders({
|
||||
'x-pid': activeProjectId.data.value || '',
|
||||
'x-integration': props.integration_id
|
||||
})
|
||||
});
|
||||
|
||||
if (!integrationData) {
|
||||
supabaseError.value = 'Cannot get integration data';
|
||||
supabaseFetching.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await getRemoteData(
|
||||
integrationData.table_name,
|
||||
integrationData.xField,
|
||||
integrationData.yMode,
|
||||
integrationData.from.toString(),
|
||||
integrationData.to.toString(),
|
||||
integrationData.slice,
|
||||
);
|
||||
if (data.error) {
|
||||
supabaseError.value = data.error;
|
||||
supabaseFetching.value = false;
|
||||
return;
|
||||
}
|
||||
supabaseFetching.value = false;
|
||||
supabaseData.value = data.result;
|
||||
|
||||
chartData.value.labels = data.result?.labels || [];
|
||||
chartData.value.datasets[0].data = data.result?.data || [];
|
||||
|
||||
console.log(data.result);
|
||||
createGradient();
|
||||
} catch (ex: any) {
|
||||
if (!ex.response._data) {
|
||||
supabaseError.value = ex.message.toString();
|
||||
supabaseFetching.value = false;
|
||||
} else {
|
||||
supabaseError.value = ex.response._data.message.toString();
|
||||
supabaseFetching.value = false;
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const { lineChartProps, lineChartRef } = useLineChart({ chartData: chartData, options: chartOptions });
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div v-if="!supabaseFetching">
|
||||
<div v-if="!supabaseError">
|
||||
<LineChart ref="lineChartRef" v-bind="lineChartProps"> </LineChart>
|
||||
</div>
|
||||
<div v-if="supabaseError"> {{ supabaseError }} </div>
|
||||
</div>
|
||||
<div v-if="supabaseFetching">
|
||||
Getting remote data...
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,6 +4,7 @@
|
||||
const ACCESS_TOKEN_STATE_KEY = 'access_token';
|
||||
const ACCESS_TOKEN_COOKIE_KEY = 'access_token';
|
||||
|
||||
|
||||
export function signHeaders(headers?: Record<string, string>) {
|
||||
const { token } = useAccessToken()
|
||||
return { headers: { ...(headers || {}), 'Authorization': 'Bearer ' + token.value } }
|
||||
|
||||
125
dashboard/composables/useSupabase.ts
Normal file
125
dashboard/composables/useSupabase.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
|
||||
|
||||
import type { TSupabaseIntegration } from "@schema/integrations/SupabaseIntegrationSchema";
|
||||
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
import { format } from 'date-fns';
|
||||
|
||||
const activeProjectId = useActiveProjectId();
|
||||
|
||||
|
||||
const computedHeaders = computed<Record<string, string>>(() => {
|
||||
const signedHeaders = signHeaders();
|
||||
return {
|
||||
'x-pid': activeProjectId.data.value || '',
|
||||
'Authorization': signedHeaders.headers.Authorization
|
||||
}
|
||||
})
|
||||
|
||||
const integrationsCredentials = useFetch(`/api/integrations/credentials/get`, {
|
||||
headers: computedHeaders,
|
||||
onResponse: (e) => {
|
||||
supabaseUrl.value = e.response._data.supabase.url || '';
|
||||
supabaseAnonKey.value = e.response._data.supabase.anon_key || '';
|
||||
supabaseServiceRoleKey.value = e.response._data.supabase.service_role_key || '';
|
||||
}
|
||||
});
|
||||
|
||||
const supabaseUrl = ref<string>('');
|
||||
const supabaseAnonKey = ref<string>('');
|
||||
const supabaseServiceRoleKey = ref<string>('');
|
||||
|
||||
const supabaseIntegrations = useFetch<TSupabaseIntegration[]>('/api/integrations/supabase/list', { headers: computedHeaders })
|
||||
|
||||
|
||||
const subabaseClientData: { client: SupabaseClient | undefined } = {
|
||||
client: undefined
|
||||
}
|
||||
|
||||
async function updateIntegrationsCredentails(data: { supabase_url: string, supabase_anon_key: string, supabase_service_role_key: string }) {
|
||||
try {
|
||||
await $fetch(`/api/integrations/credentials/${activeProjectId.data.value}/update`, {
|
||||
...signHeaders({ 'Content-Type': 'application/json' }),
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
supabase_url: data.supabase_url,
|
||||
supabase_anon_key: data.supabase_anon_key,
|
||||
supabase_service_role_key: data.supabase_service_role_key
|
||||
}),
|
||||
});
|
||||
integrationsCredentials.refresh();
|
||||
return { ok: true, error: '' }
|
||||
} catch (ex: any) {
|
||||
return { ok: false, error: ex.message.toString() };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function createSupabaseUrl(supabaseUrl: string) {
|
||||
let result = supabaseUrl;
|
||||
if (!result.includes('https://')) result = `https://${result}`;
|
||||
if (!result.endsWith('.supabase.co')) result = `${result}.supabase.co`;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
async function testConnection() {
|
||||
const url = createSupabaseUrl(supabaseUrl.value);
|
||||
subabaseClientData.client = createClient(url, supabaseAnonKey.value);
|
||||
const res = await subabaseClientData.client.from('_t_e_s_t_').select('*').limit(1);
|
||||
if (res.error?.message.startsWith('TypeError')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
type GroupBy = 'day' | 'month';
|
||||
|
||||
const groupByDate = (data: string[], groupBy: GroupBy) => {
|
||||
return data.reduce((acc, item) => {
|
||||
const date = new Date(item);
|
||||
const dateKey = groupBy === 'day'
|
||||
? format(date, 'yyyy-MM-dd') // Group by day
|
||||
: format(date, 'yyyy-MM'); // Group by month
|
||||
|
||||
if (!acc[dateKey]) { acc[dateKey] = []; }
|
||||
|
||||
acc[dateKey].push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>);
|
||||
}
|
||||
|
||||
async function getRemoteData(table: string, xField: string, yMode: string, from: string, to: string, slice: string) {
|
||||
const url = createSupabaseUrl(supabaseUrl.value);
|
||||
subabaseClientData.client = createClient(url, supabaseAnonKey.value);
|
||||
const res = await subabaseClientData.client.from(table).select(xField)
|
||||
.filter(xField, 'gte', from)
|
||||
.filter(xField, 'lte', to);
|
||||
|
||||
if (res.error) return { error: res.error.message };
|
||||
|
||||
const grouped = groupByDate(res.data.map((e: any) => e.created_at) || [], slice as any);
|
||||
|
||||
const result: { labels: string[], data: number[] } = { labels: [], data: [] }
|
||||
|
||||
for (const key in grouped) {
|
||||
result.labels.push(key);
|
||||
result.data.push(grouped[key].length);
|
||||
}
|
||||
|
||||
|
||||
return { result };
|
||||
}
|
||||
|
||||
export function useSupabase() {
|
||||
|
||||
return {
|
||||
getRemoteData,
|
||||
testConnection,
|
||||
supabaseIntegrations, integrationsCredentials,
|
||||
supabaseUrl, supabaseAnonKey,
|
||||
supabaseServiceRoleKey,
|
||||
updateIntegrationsCredentails
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const sections: Section[] = [
|
||||
{ label: 'Security', to: '/security', icon: 'fal fa-shield' },
|
||||
{ label: 'Insights (soon)', to: '#', icon: 'fal fa-lightbulb', disabled: true },
|
||||
{ label: 'Links (soon)', to: '#', icon: 'fal fa-globe-pointer', disabled: true },
|
||||
{ label: 'Integrations (soon)', to: '#', icon: 'fal fa-cube', disabled: true },
|
||||
{ label: 'Integrations (soon)', to: '/integrations', icon: 'fal fa-cube', disabled: true },
|
||||
{ label: 'Settings', to: '/settings', icon: 'fal fa-gear' },
|
||||
{
|
||||
grow: true,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@getbrevo/brevo": "^2.2.0",
|
||||
"@nuxtjs/tailwindcss": "^6.12.0",
|
||||
"@supabase/supabase-js": "^2.45.4",
|
||||
"chart.js": "^3.9.1",
|
||||
"chartjs-chart-funnel": "^4.2.1",
|
||||
"chartjs-plugin-annotation": "^2.2.1",
|
||||
|
||||
105
dashboard/pages/integrations.vue
Normal file
105
dashboard/pages/integrations.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import SupabaseChartDialog from '~/components/integrations/SupabaseChartDialog.vue';
|
||||
|
||||
definePageMeta({ layout: 'dashboard' });
|
||||
const activeProjectId = useActiveProjectId();
|
||||
|
||||
|
||||
const { createAlert } = useAlert();
|
||||
|
||||
const {
|
||||
supabaseUrl, supabaseAnonKey, supabaseServiceRoleKey, integrationsCredentials,
|
||||
supabaseIntegrations, updateIntegrationsCredentails
|
||||
} = useSupabase()
|
||||
|
||||
async function updateCredentials() {
|
||||
|
||||
const res = await updateIntegrationsCredentails({
|
||||
supabase_url: supabaseUrl.value,
|
||||
supabase_anon_key: supabaseAnonKey.value,
|
||||
supabase_service_role_key: supabaseServiceRoleKey.value
|
||||
});
|
||||
|
||||
if (res.ok === true) {
|
||||
integrationsCredentials.refresh();
|
||||
createAlert('Credentials updated', 'Credentials updated successfully', 'far fa-error', 4000);
|
||||
} else {
|
||||
createAlert('Error updating credentials', res.error, 'far fa-error', 4000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const { openDialogEx } = useCustomDialog()
|
||||
|
||||
function showChartDialog() {
|
||||
openDialogEx(SupabaseChartDialog, {
|
||||
closable: true,
|
||||
width: '55vw',
|
||||
height: '65vh'
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<div class="home w-full h-full px-10 pt-6 overflow-y-auto">
|
||||
|
||||
<CardTitled title="Supabase integration" class="w-full">
|
||||
<template #header>
|
||||
<img class="h-10 w-10" :src="'supabase.svg'" alt="Supabase logo">
|
||||
</template>
|
||||
<div class="flex gap-6 flex-col w-full">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-lyx-text"> Supabase url </div>
|
||||
<div class="text-lyx-text-dark"> Required to fetch data from supabase </div>
|
||||
<LyxUiInput v-if="!integrationsCredentials.pending.value" class="w-full mt-2 px-4 py-1"
|
||||
v-model="supabaseUrl" type="text"></LyxUiInput>
|
||||
<div v-if="integrationsCredentials.pending.value"> Loading... </div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-lyx-text"> Supabase anon key </div>
|
||||
<div class="text-lyx-text-dark"> Required to fetch data from supabase </div>
|
||||
<LyxUiInput v-if="!integrationsCredentials.pending.value" class="w-full mt-2 px-4 py-1"
|
||||
v-model="supabaseAnonKey" type="password"></LyxUiInput>
|
||||
<div v-if="integrationsCredentials.pending.value"> Loading... </div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-lyx-text"> Supabase service role key </div>
|
||||
<div class="text-lyx-text-dark"> Only used if you need to bypass RLS </div>
|
||||
<LyxUiInput v-if="!integrationsCredentials.pending.value" class="w-full mt-2 px-4 py-1"
|
||||
v-model="supabaseServiceRoleKey" type="password"></LyxUiInput>
|
||||
<div v-if="integrationsCredentials.pending.value"> Loading... </div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<LyxUiButton v-if="!integrationsCredentials.pending.value" @click="updateCredentials()"
|
||||
type="primary"> Save
|
||||
</LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</CardTitled>
|
||||
|
||||
|
||||
<LyxUiCard class="mt-6 w-full">
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex gap-2 items-center" v-for="supabaseIntegration of supabaseIntegrations.data.value">
|
||||
<div> {{ supabaseIntegration.name }} </div>
|
||||
<div> <i class="far fa-edit"></i> </div>
|
||||
<div> <i class="far fa-trash"></i> </div>
|
||||
</div>
|
||||
<div>
|
||||
<LyxUiButton type="primary" @click="showChartDialog()"> Add supabase chart </LyxUiButton>
|
||||
</div>
|
||||
</div>
|
||||
</LyxUiCard>
|
||||
|
||||
|
||||
<div class="mt-10">
|
||||
<IntegrationsSupabaseLineChart integration_id="66f6c558d97e4abd408feee0"></IntegrationsSupabaseLineChart>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
86
dashboard/pnpm-lock.yaml
generated
86
dashboard/pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
||||
'@nuxtjs/tailwindcss':
|
||||
specifier: ^6.12.0
|
||||
version: 6.12.0(rollup@4.18.0)
|
||||
'@supabase/supabase-js':
|
||||
specifier: ^2.45.4
|
||||
version: 2.45.4
|
||||
chart.js:
|
||||
specifier: ^3.9.1
|
||||
version: 3.9.1
|
||||
@@ -1138,6 +1141,28 @@ packages:
|
||||
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@supabase/auth-js@2.65.0':
|
||||
resolution: {integrity: sha512-+wboHfZufAE2Y612OsKeVP4rVOeGZzzMLD/Ac3HrTQkkY4qXNjI6Af9gtmxwccE5nFvTiF114FEbIQ1hRq5uUw==}
|
||||
|
||||
'@supabase/functions-js@2.4.1':
|
||||
resolution: {integrity: sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==}
|
||||
|
||||
'@supabase/node-fetch@2.6.15':
|
||||
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
|
||||
'@supabase/postgrest-js@1.16.1':
|
||||
resolution: {integrity: sha512-EOSEZFm5pPuCPGCmLF1VOCS78DfkSz600PBuvBND/IZmMciJ1pmsS3ss6TkB6UkuvTybYiBh7gKOYyxoEO3USA==}
|
||||
|
||||
'@supabase/realtime-js@2.10.2':
|
||||
resolution: {integrity: sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==}
|
||||
|
||||
'@supabase/storage-js@2.7.0':
|
||||
resolution: {integrity: sha512-iZenEdO6Mx9iTR6T7wC7sk6KKsoDPLq8rdu5VRy7+JiT1i8fnqfcOr6mfF2Eaqky9VQzhP8zZKQYjzozB65Rig==}
|
||||
|
||||
'@supabase/supabase-js@2.45.4':
|
||||
resolution: {integrity: sha512-E5p8/zOLaQ3a462MZnmnz03CrduA5ySH9hZyL03Y+QZLIOO4/Gs8Rdy4ZCKDHsN7x0xdanVEWWFN3pJFQr9/hg==}
|
||||
|
||||
'@swc/helpers@0.3.17':
|
||||
resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==}
|
||||
|
||||
@@ -1223,6 +1248,9 @@ packages:
|
||||
'@types/pdfkit@0.13.4':
|
||||
resolution: {integrity: sha512-ixGNDHYJCCKuamY305wbfYSphZ2WPe8FPkjn8oF4fHV+PgPV4V+hecPh2VOS2h4RNtpSB3zQcR4sCpNvvrEb1A==}
|
||||
|
||||
'@types/phoenix@1.6.5':
|
||||
resolution: {integrity: sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==}
|
||||
|
||||
'@types/qs@6.9.16':
|
||||
resolution: {integrity: sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==}
|
||||
|
||||
@@ -1244,6 +1272,9 @@ packages:
|
||||
'@types/whatwg-url@11.0.5':
|
||||
resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==}
|
||||
|
||||
'@types/ws@8.5.12':
|
||||
resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==}
|
||||
|
||||
'@ungap/structured-clone@1.2.0':
|
||||
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
|
||||
|
||||
@@ -1491,9 +1522,6 @@ packages:
|
||||
'@vue/shared@3.4.27':
|
||||
resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==}
|
||||
|
||||
'@vue/shared@3.5.7':
|
||||
resolution: {integrity: sha512-NBE1PBIvzIedxIc2RZiKXvGbJkrZ2/hLf3h8GlS4/sP9xcXEZMFWOazFkNd6aGeUCMaproe5MHVYB3/4AW9q9g==}
|
||||
|
||||
'@vue/shared@3.5.8':
|
||||
resolution: {integrity: sha512-mJleSWbAGySd2RJdX1RBtcrUBX6snyOc0qHpgk3lGi4l9/P/3ny3ELqFWqYdkXIwwNN/kdm8nD9ky8o6l/Lx2A==}
|
||||
|
||||
@@ -6632,6 +6660,48 @@ snapshots:
|
||||
|
||||
'@sindresorhus/merge-streams@2.3.0': {}
|
||||
|
||||
'@supabase/auth-js@2.65.0':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/functions-js@2.4.1':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/node-fetch@2.6.15':
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
'@supabase/postgrest-js@1.16.1':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/realtime-js@2.10.2':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
'@types/phoenix': 1.6.5
|
||||
'@types/ws': 8.5.12
|
||||
ws: 8.17.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@supabase/storage-js@2.7.0':
|
||||
dependencies:
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
|
||||
'@supabase/supabase-js@2.45.4':
|
||||
dependencies:
|
||||
'@supabase/auth-js': 2.65.0
|
||||
'@supabase/functions-js': 2.4.1
|
||||
'@supabase/node-fetch': 2.6.15
|
||||
'@supabase/postgrest-js': 1.16.1
|
||||
'@supabase/realtime-js': 2.10.2
|
||||
'@supabase/storage-js': 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@swc/helpers@0.3.17':
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
@@ -6719,6 +6789,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.12.12
|
||||
|
||||
'@types/phoenix@1.6.5': {}
|
||||
|
||||
'@types/qs@6.9.16': {}
|
||||
|
||||
'@types/resize-observer-browser@0.1.11': {}
|
||||
@@ -6735,6 +6807,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/webidl-conversions': 7.0.3
|
||||
|
||||
'@types/ws@8.5.12':
|
||||
dependencies:
|
||||
'@types/node': 20.12.12
|
||||
|
||||
'@ungap/structured-clone@1.2.0': {}
|
||||
|
||||
'@unhead/dom@1.9.11':
|
||||
@@ -7152,7 +7228,7 @@ snapshots:
|
||||
'@volar/language-core': 1.11.1
|
||||
'@volar/source-map': 1.11.1
|
||||
'@vue/compiler-dom': 3.4.27
|
||||
'@vue/shared': 3.5.7
|
||||
'@vue/shared': 3.5.8
|
||||
computeds: 0.0.1
|
||||
minimatch: 9.0.4
|
||||
muggle-string: 0.3.1
|
||||
@@ -7200,8 +7276,6 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.4.27': {}
|
||||
|
||||
'@vue/shared@3.5.7': {}
|
||||
|
||||
'@vue/shared@3.5.8': {}
|
||||
|
||||
'@vueuse/components@10.10.0(vue@3.4.27(typescript@5.4.2))':
|
||||
|
||||
15
dashboard/public/supabase.svg
Normal file
15
dashboard/public/supabase.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="98" height="100" viewBox="0 0 98 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M56.8944 98.338C54.3397 101.555 49.1599 99.7924 49.0983 95.6846L48.1982 35.6025H88.5973C95.9147 35.6025 99.9957 44.0542 95.4457 49.7849L56.8944 98.338Z" fill="url(#paint0_linear_99_24683)"/>
|
||||
<path d="M56.8944 98.338C54.3397 101.555 49.1599 99.7924 49.0983 95.6846L48.1982 35.6025H88.5973C95.9147 35.6025 99.9957 44.0542 95.4457 49.7849L56.8944 98.338Z" fill="url(#paint1_linear_99_24683)" fill-opacity="0.2"/>
|
||||
<path d="M40.464 1.66109C43.0187 -1.55638 48.1986 0.206562 48.2601 4.31445L48.6546 64.3964H8.76106C1.44348 64.3964 -2.63767 55.9448 1.91262 50.214L40.464 1.66109Z" fill="#3ECF8E"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_99_24683" x1="48.1982" y1="48.9242" x2="84.1036" y2="63.9829" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#249361"/>
|
||||
<stop offset="1" stop-color="#3ECF8E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_99_24683" x1="32.2797" y1="27.1289" x2="48.6544" y2="57.9534" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
23
dashboard/server/api/integrations/credentials/get.ts
Normal file
23
dashboard/server/api/integrations/credentials/get.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { IntegrationsCredentialsModel } from '@schema/integrations/IntegrationsCredentialsSchema';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getHeader(event, 'x-pid');
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const credentials = await IntegrationsCredentialsModel.findOne({ project_id });
|
||||
|
||||
return {
|
||||
supabase: {
|
||||
anon_key: credentials?.supabase_anon_key || '',
|
||||
service_role_key: credentials?.supabase_service_role_key || '',
|
||||
url: credentials?.supabase_url || ''
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
23
dashboard/server/api/integrations/credentials/update.post.ts
Normal file
23
dashboard/server/api/integrations/credentials/update.post.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { IntegrationsCredentialsModel } from "@schema/integrations/IntegrationsCredentialsSchema";
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getHeader(event, 'x-pid');
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
const res = await IntegrationsCredentialsModel.updateOne({ project_id }, {
|
||||
supabase_anon_key: body.supabase_anon_key || '',
|
||||
supabase_service_role_key: body.supabase_service_role_key || '',
|
||||
supabase_url: body.supabase_url || '',
|
||||
}, { upsert: true });
|
||||
|
||||
return { ok: res.acknowledged };
|
||||
|
||||
});
|
||||
29
dashboard/server/api/integrations/supabase/add.post.ts
Normal file
29
dashboard/server/api/integrations/supabase/add.post.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { SupabaseIntegrationModel } from "@schema/integrations/SupabaseIntegrationSchema";
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getHeader(event, 'x-pid');
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const { chart_type, table_name, xField, yMode, from, to, slice, name } = await readBody(event);
|
||||
|
||||
if (!project.premium) {
|
||||
const supabaseIntegrationsCount = await SupabaseIntegrationModel.countDocuments({ project_id });
|
||||
if (supabaseIntegrationsCount > 0) return setResponseStatus(event, 400, 'LIMIT_REACHED');
|
||||
}
|
||||
|
||||
await SupabaseIntegrationModel.create({
|
||||
name,
|
||||
project_id, chart_type,
|
||||
table_name, xField, yMode,
|
||||
from, to, slice,
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
|
||||
});
|
||||
18
dashboard/server/api/integrations/supabase/get.ts
Normal file
18
dashboard/server/api/integrations/supabase/get.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { SupabaseIntegrationModel } from '@schema/integrations/SupabaseIntegrationSchema';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getHeader(event, 'x-pid');
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const integration_id = getHeader(event, 'x-integration');
|
||||
|
||||
const integration = await SupabaseIntegrationModel.findOne({ _id: integration_id });
|
||||
return integration;
|
||||
|
||||
});
|
||||
16
dashboard/server/api/integrations/supabase/list.ts
Normal file
16
dashboard/server/api/integrations/supabase/list.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
|
||||
import { SupabaseIntegrationModel } from '@schema/integrations/SupabaseIntegrationSchema';
|
||||
|
||||
export default defineEventHandler(async event => {
|
||||
|
||||
const project_id = getHeader(event, 'x-pid');
|
||||
if (!project_id) return;
|
||||
|
||||
const user = getRequestUser(event);
|
||||
const project = await getUserProjectFromId(project_id, user);
|
||||
if (!project) return;
|
||||
|
||||
const integrations = await SupabaseIntegrationModel.find({ project_id });
|
||||
return integrations;
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user