8

Dashboard Template

Hello everyone!

After the    post, I decided to start a new one. This will make it easier to follow the changes.

New features have been added:

  • Sticky buttons. Thanks to   CSS hacks, I was able to make the buttons component sticky. Now the buttons always remain visible to the top of the page as we scroll down.
  • Scroll into View. By clicking a button the page automatically scrolls into the correspoding section. In the attached database press the first button "Accounts" to see it in action.

The combination of the above two features has the potential to transform the way we design our Dashboards. Like the one-page websites, we can present our content in sections on one long Dashboard.

In Safari works but I haven't check it in other browsers. Let me know if you find any bug.

What other functionalities would you like to see?

Leave here your suggestions, ideas or improvements!

Enjoy!

181 replies

null
    • szormpas
    • 2 mths ago
    • Reported - view

    Version 7.3

    Map improvements:

    Hi everyone, custom markers are a great way to get creative since any image can be a marker on our map. However, if you want each point to have its unique one, it can become a bit time-consuming since you must introduce a new PNG file for each. In this version, I've added a few cool features to make the map more functional.

    • Circle markers: The component lets you draw circles with a radius you specify in pixels (so the size in meters changes with zoom). You can use it to set the radius, width, colour, fill colour, opacity, and so on.
    • GeoJSON: This is a special type of JSON object that's used to represent vector geometries, with or without non-spatial attributes. GeoJSON is a really popular format for mapping data, and Leaflet is great at handling it. Since Ninox scripting makes it so easy to construct JSON objects, we can present any data from our database on the map in whatever way we want. On top of that, we can also import external GeoJSONs. If you want to create your own, there's a really easy way to do it with geojson.io.
    • Multiple maps: You can load a few different base maps, so the user can choose one that suits them best.
    • Filtering and grouping: The library can filter the data based on geojson properties, which means we can create groups and show them on the map in a selective way.

    In my example, you'll find a layer control panel in the top right corner where you can choose a base map and filter the markers based on the status field of the Accounts table.

    Enjoy!🙂

      • szormpas
      • 2 mths ago
      • Reported - view

        Hi, in this setup, there are four public URL links. There's one for each of the red, green and blue icon markers and one for the common shadow icon, as shown below:

      var redIcon = new LeafIcon({iconUrl: 'https://share.ninox.com/9kdnfo84wtpootgow6px1g3golu874l9i9ec'}),
          greenIcon = new LeafIcon({iconUrl: 'https://share.ninox.com/1jiqvo5pdsato3qmazp5h5nfodnbs2us9n3f'}),
          blueIcon = new LeafIcon({iconUrl: 'https://share.ninox.com/k0670kbjvl6nzc2xhpjw2zjjq5j6hbjqyns0'});
      
      shadowUrl: 'https://share.ninox.com/lq774ccvc9kw89w699n20ea95ho2o2bqbpt0'
      

      As a general rule, you can put the custom icon files in the same directory as leaflet.css or in a valid URL. When it comes to Ninox, the only option is to use URLs.

      So, how can we create these URLs inside Ninox? The first thing you need to do is save each icon file as an image. Then, just use the 'Share this file...' option to create a public, shareable URL like the one above.

      You only need to create these links once. As long as you don't delete the files, the links will stay valid. Just a heads-up: if you delete the original database and import it again, you'll need to repeat the above process.

      You also asked if it's possible to share the map with clients even if they don't have Ninox. Could you give us a bit more info on how you'd like this to work?

      • gold_cat
      • 2 mths ago
      • Reported - view

       hi,
      I noticed that this website also uses the open-source map you shared. I’m very interested in the red area feature displayed on the map. Can this be used in this great database? Thank you.

      • Kruna
      • 2 mths ago
      • Reported - view

       ok, thank you very much for your explanation - understood now!👍

      Well, I wonder if I can provide kind of (shared)link like in google maps, so clients can see the map too. On the other hand they would need ninox to see the records when clicking on iconcs, so I guess I would need to buy licenses for clients and that wouldnt be worth it , so I will need to stay with google maps.

      • szormpas
      • 2 mths ago
      • Reported - view

        Hi there!

      I just wanted to let you know that this map display is called a choropleth map. The great news is that you can implement it with Leaflet!

      I've included some useful links below to help you get started if you want to create this kind of map:

      • gold_cat
      • 2 mths ago
      • Reported - view

       Thank you for your help.

      • gold_cat
      • 2 mths ago
      • Reported - view

       
      Hi friend, I found that the database cannot use the following code in the 'On Open' trigger. Is this an issue?

      openTable("Dashboard")
      
      • szormpas
      • 2 mths ago
      • Reported - view

         Hi, the 'Dashboard' is a Page, so try this:

      openPage("Dashboard")

      • gold_cat
      • 2 mths ago
      • Reported - view

        thank you friend~

      • gold_cat
      • 1 mth ago
      • Reported - view

       
      Hello friend, I have added the "cluster maps" feature into the code below, but it doesn't display the aggregated statistical numbers. What could be the problem?

      let _accounts := (select Accounts);
      let myGeoJson := {
              type: "FeatureCollection",
              features: _accounts.{
                  type: "Feature",
                  properties: {
                      id: raw(Id),
                      name: "<b>" + 'First Name' + "  " + 'Last Name' + ",  " + age(DOB) + " yrs" + "</b>",
                      status: text(Status)
                  },
                  geometry: {
                      type: "Point",
                      coordinates: [longitude(Address), latitude(Address)]
                  }
              }
          };
      html("
      
       <link rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'
           integrity='sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY='
           crossorigin=''/>
      
       <script src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'
           integrity='sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo='
           crossorigin=''></script>
      
       <script src='https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/leaflet.markercluster.js'></script>
       <link href='https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/MarkerCluster.css' rel='stylesheet' />
       <link href='https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/MarkerCluster.Default.css' rel='stylesheet' />
      
       <div id='map' style='height: 100%; width: 100%;'></div>
      
       <script>
           setTimeout(function(){
      
               var alidadeSmooth = L.tileLayer('" +
      Settings[Id = 5].Value +
      "', {
                    maxZoom: 20,
                    attribution: '" +
      Settings[Id = 6].Value +
      "'
      });
      
               var stadiaOutdoors = L.tileLayer('" +
      Settings[Id = 7].Value +
      "', {
                    maxZoom: 20,
                    attribution: '" +
      Settings[Id = 6].Value +
      "'
      });
      
               var stamenToner = L.tileLayer('" +
      Settings[Id = 8].Value +
      "', {
                    maxZoom: 20,
                    attribution: '" +
      Settings[Id = 6].Value +
      "'
      });
      
              var map = L.map('map')
                     .addLayer(alidadeSmooth)
                  .setView([40.416775, -3.703790], 6);
      
              var LeafIcon = L.Icon.extend({
                                  options: {
                                  shadowUrl: 'https://share.ninox.com/m3s3wdhmqxasol3sukknnu51lok48u7ndf7l',
                                  iconSize:     [27.6, 44.6],
                                  iconAnchor:   [13.8, 44.6],
                                  shadowSize:   [41, 41],
                                  shadowAnchor: [10, 41],
                                  }
              });
      
              var redIcon = new LeafIcon({iconUrl: 'https://share.ninox.com/n6z5enqxwdjjg97hkbs7vwpoygnkmzp8ok67'}),
                  greenIcon = new LeafIcon({iconUrl: 'https://share.ninox.com/b3d6tidb2syba20fyaevy4n3hpv0saecpilj'}),
                  blueIcon = new LeafIcon({iconUrl: 'https://share.ninox.com/18zvxhey2pf7ichgppv5fb275monke6pt6p7'});
      
              function onEachFeature(feature, layer) {
                 layer.bindTooltip(feature.properties.name);
                 layer.on('click', function(e) {
                          ui.popupRecord(feature.properties.id);
                 });
              };
      
              var markers = L.markerClusterGroup();
      
              var activeStatusLayer = L.geoJSON(" +
      myGeoJson +
      ", {
          filter: function(feature, layer) {
          return (feature.properties.status === 'Active');
          },
          pointToLayer: function (feature, latlng) {
              return L.marker(latlng, {icon: greenIcon});
          },
          style: {color: '#00FF00'},
          onEachFeature: onEachFeature
      });
      
              var suspendedStatusLayer = L.geoJSON(" +
      myGeoJson +
      ", {
          filter: function(feature, layer) {
          return (feature.properties.status === 'Suspended');
          },
          pointToLayer: function (feature, latlng) {
              return L.marker(latlng, {icon: blueIcon});
          },
          style: {color: '#005AFF'},
          onEachFeature: onEachFeature
      });
      
              var deactivatedStatusLayer = L.geoJSON(" +
      myGeoJson +
      ", {
          filter: function(feature, layer) {
          return (feature.properties.status === 'Deactivated');
          },
          pointToLayer: function (feature, latlng) {
              return L.marker(latlng, {icon: redIcon});
          },
          style: {color: '#FF0000'},
          onEachFeature: onEachFeature
      });
      
              markers.addLayer(activeStatusLayer);
              markers.addLayer(suspendedStatusLayer);
              markers.addLayer(deactivatedStatusLayer);
      
              map.addLayer(markers);
      
              var baseMaps = {
                   'Alidade Smooth': alidadeSmooth,
                   'Stadia Outdoors': stadiaOutdoors,
                   'Stamen Toner': stamenToner,
              };
      
              var overlayMaps = {
                   '<span style=""color: #00FF00"">Active</span>': activeStatusLayer,
                   '<span style=""color: #005AFF"">Suspended</span>': suspendedStatusLayer,
                   '<span style=""color: #FF0000"">Deactivated</span>': deactivatedStatusLayer,
              };
      
              var layerControl = L.control.layers(baseMaps, overlayMaps).addTo(map);
      
           },1000);
       </script>
      
      ")
      • szormpas
      • 1 mth ago
      • Reported - view

         Hi, this is an advanced implementation of the Leaflet library. 

      From the link provided, we can see that it depends on a lot of external libraries to work (six in total beyond Leaflet!).

      It also requires a subscription to MapTiler to get your own API key.

      I suggest you try to get the example code from the provided link to work in Ninox to see if this is feasible at all.

      • gold_cat
      • 1 mth ago
      • Reported - view

       

      It seems more complicated than I thought. Could you please help me with another question? How can I set the map to display only the global range, excluding the area within the red box?

      • szormpas
      • 1 mth ago
      • Reported - view

        Hi, you should increase the initial zoom level of the map.

      • gold_cat
      • 1 mth ago
      • Reported - view

       

      Is it possible to display only one map overall, regardless of the zoom level?

      • szormpas
      • 1 mth ago
      • Reported - view

        A basic zoom control with two buttons (zoom in and zoom out) it is put on the map by default unless you set its zoomControl option to false. Try something like below:

      var map = new L.map('map', { zoomControl: false });
      • gold_cat
      • 1 mth ago
      • Reported - view

       Hi friend, maybe the translation didn’t quite capture what I meant. I’ve made the following GIF to show what I mean. When the map is zoomed out to the smallest level, multiple map tiles appear. However, I only want to display one complete globe map. Is this achievable?

      • szormpas
      • 1 mth ago
      • Reported - view

        you have to add a 'minZoom: 3' property in addition to 'maxZoom: 20' property.

      • gold_cat
      • 1 mth ago
      • Reported - view

       thanks'

    • szormpas
    • 2 mths ago
    • Reported - view

    Version 8.0

    Calendar (scheduler):

    Hello everyone,

    As we all know, Ninox's built-in calendar is pretty basic and has limited capabilities. I thought that if we integrated a custom calendar directly within the dashboard, we could add features that would enhance the user experience.

    FullCalendar.io is a JavaScript library that provides its core as open source. The good news is that its code seems to be quite compatible with Ninox. I was able to fully integrate it and store it inside the Ninox database (no dependency on CDN links).

    I've been able to develop the following features so far:

    • You can upload and display data directly from the Ninox database (event sources).
    • When you hover the mouse over each event, a tooltip will appear (thanks to the hint.css library).
    • When you click on an event, the corresponding record will pop up.
    • To schedule a new event, just choose one or more free slots in the calendar. A new record will be created automatically and populated with the corresponding dates (thanks to  for sharing the use of the fetch method in this post).
    • If you need to change the time of an event, just drag and drop it into a different time slot. The record in the Ninox database will be updated right away.
    • You can edit the events on the calendar, which means you can move and resize their start and/or end times.
    • I've added three views (month, week, day), and I've got more in the pipeline!

    I'm thrilled to announce the launch of the first free, open-source scheduler for Ninox! ☺️

      • szormpas
      • 2 mths ago
      • Reported - view

         

      I forgot to delete my Ninox API Key from the shared database. For security reasons, I had to delete it. If you try to create a new or modify an existing event inside the calendar, you'll get an alert saying "HTTP status error: 401 - Unauthorized."

      To unlock the scheduler capabilities, you just need to create your API Key (Personal Access Token) and paste it into the respective Value field in the 'Settings' table.

      Enjoy!

      • Kruna
      • 1 mth ago
      • Reported - view

       good morning - I have some problems adapting this awesome calendar! Its amazing what you create and share in Ninox - chapeau!

      Here is my adapted code - no error is shown, but still the calendar wont show up.

      let _events := x.{
              id: number(Nr),
              title: let t := this;
              (select '2. Standortvorschläge' where Nr = t.'Standortvorschlag zu Firma/Eigentümer').(Vorname + "  " + Nachname),
              start: format('erstellt am', "YYYY-MM-DDTHH:mm"),
              end: format('Fällig am', "YYYY-MM-DDTHH:mm"),
              extendedProps: {
                  description: Informationen
              }
      

      I believe that the issue is here

              (select '2. Standortvorschläge' where Nr = t.'Standortvorschlag zu Firma/Eigentümer').(Vorname + "  " + Nachname),
       

      In your version

      Nr = number(t.Name)).('First Name' + " " + 'Last Name')

      the number(t.Name)) its a dynamic choice field, in my case its a linked table.

      Do you have an idea what might be wrong in my script?

      thanks Kruna

      • szormpas
      • 1 mth ago
      • Reported - view

        Hi,

      You are right, 'Name' is a dynamic choice field that gets the ('First Name' + " " + 'Last Name') from the 'Accounts' table.

      In your setup you don't need a second select statement since the 'Aufgaben' table is linked to 'Standortvorschlag zu Firma/Eigentümer' table (N:1).

      You can try the following:

      let x := (select Aufgaben);
      let _events := x.{
              id: number(Nr),
              title: 'Standortvorschlag zu Firma/Eigentümer').(Vorname + "  " + Nachname),
              start: format('erstellt am', "YYYY-MM-DDTHH:mm"),
              end: format('Fällig am', "YYYY-MM-DDTHH:mm"),
              extendedProps: {
                  description: Informationen
              }
      • Kruna
      • 1 mth ago
      • Reported - view

       I must be missing something because calendar wont show up.😫

      • Kruna
      • 1 mth ago
      • Reported - view

       now it shows some errors, which hadnt been there before.

      • Kruna
      • 1 mth ago
      • Reported - view

       does look the same, doesnt it?!

      • szormpas
      • 1 mth ago
      • Reported - view

        Hi,

      I think you should try putting in plain text instead of the title to see if that's what's causing the problem:

      title: "Test",

Content aside

  • 8 Likes
  • 2 days agoLast active
  • 181Replies
  • 1786Views
  • 12 Following